1 /*
2 * Copyright (C) 2021, The Android Open Source Project
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 #include "comments.h"
17
18 #include <android-base/result.h>
19 #include <android-base/strings.h>
20
21 #include <optional>
22 #include <regex>
23 #include <string>
24 #include <vector>
25
26 #include "logging.h"
27
28 using android::base::EndsWith;
29 using android::base::Error;
30 using android::base::Join;
31 using android::base::Result;
32 using android::base::Split;
33 using android::base::StartsWith;
34 using android::base::Trim;
35
36 namespace android {
37 namespace aidl {
38
39 namespace {
40
41 static const std::string_view kLineCommentBegin = "//";
42 static const std::string_view kBlockCommentBegin = "/*";
43 static const std::string_view kBlockCommentMid = " *";
44 static const std::string_view kBlockCommentEnd = "*/";
45 static const std::string_view kDocCommentBegin = "/**";
46 static const std::string kTagDeprecated = "@deprecated";
47 static const std::regex kTagHideRegex{"@hide\\b"};
48
ConsumePrefix(const std::string & s,std::string_view prefix)49 std::string ConsumePrefix(const std::string& s, std::string_view prefix) {
50 AIDL_FATAL_IF(!StartsWith(s, prefix), AIDL_LOCATION_HERE)
51 << "'" << s << "' has no prefix '" << prefix << "'";
52 return s.substr(prefix.size());
53 }
54
ConsumeSuffix(const std::string & s,std::string_view suffix)55 std::string ConsumeSuffix(const std::string& s, std::string_view suffix) {
56 AIDL_FATAL_IF(!EndsWith(s, suffix), AIDL_LOCATION_HERE);
57 return s.substr(0, s.size() - suffix.size());
58 }
59
60 struct BlockTag {
61 std::string name;
62 std::string description;
63 };
64
65 // Removes comment markers: //, /*, */, optional leading "*" in block comments
66 // - keeps leading spaces, but trims trailing spaces
67 // - keeps empty lines
TrimmedLines(const Comment & c)68 std::vector<std::string> TrimmedLines(const Comment& c) {
69 if (c.type == Comment::Type::LINE) {
70 return std::vector{ConsumePrefix(c.body, kLineCommentBegin)};
71 }
72
73 std::string stripped = ConsumeSuffix(ConsumePrefix(c.body, kBlockCommentBegin), kBlockCommentEnd);
74
75 std::vector<std::string> lines;
76 bool found_first_line = false;
77
78 for (auto& line : Split(stripped, "\n")) {
79 // Delete prefixes like " * ", " *", or " ".
80 size_t idx = 0;
81 for (; idx < line.size() && isspace(line[idx]); idx++)
82 ;
83 if (idx < line.size() && line[idx] == '*') idx++;
84 if (idx < line.size() && line[idx] == ' ') idx++;
85
86 const std::string& sanitized_line = line.substr(idx);
87 size_t i = sanitized_line.size();
88 for (; i > 0 && isspace(sanitized_line[i - 1]); i--)
89 ;
90
91 // Either the size is 0 or everything was whitespace.
92 bool is_empty_line = i == 0;
93
94 found_first_line = found_first_line || !is_empty_line;
95 if (!found_first_line) continue;
96
97 // if is_empty_line, i == 0 so substr == ""
98 lines.push_back(sanitized_line.substr(0, i));
99 }
100 // remove trailing empty lines
101 while (!lines.empty() && Trim(lines.back()).empty()) {
102 lines.pop_back();
103 }
104 return lines;
105 }
106
107 // Parses a block comment and returns block tags in the comment.
BlockTags(const Comment & c)108 std::vector<BlockTag> BlockTags(const Comment& c) {
109 AIDL_FATAL_IF(c.type != Comment::Type::BLOCK, AIDL_LOCATION_HERE);
110
111 std::vector<BlockTag> tags;
112
113 // current tag and paragraph
114 std::string tag;
115 std::vector<std::string> paragraph;
116
117 auto end_paragraph = [&]() {
118 if (tag.empty()) {
119 paragraph.clear();
120 return;
121 }
122 // paragraph lines are trimed at both ends
123 tags.push_back({tag, Join(paragraph, " ")});
124 tag.clear();
125 paragraph.clear();
126 };
127
128 for (const auto& line : TrimmedLines(c)) {
129 size_t idx = 0;
130 // skip leading spaces
131 for (; idx < line.size() && isspace(line[idx]); idx++)
132 ;
133
134 if (idx == line.size()) {
135 // skip empty lines
136 } else if (line[idx] == '@') {
137 // end the current paragraph before reading a new block tag (+ description paragraph)
138 end_paragraph();
139
140 size_t end_idx = idx + 1;
141 for (; end_idx < line.size() && isalpha(line[end_idx]); end_idx++)
142 ;
143
144 tag = line.substr(idx, end_idx - idx);
145
146 if (end_idx < line.size() && line[end_idx] == ' ') end_idx++;
147 // skip empty line
148 if (end_idx < line.size()) {
149 paragraph.push_back(line.substr(end_idx));
150 }
151 } else {
152 // gather paragraph lines with leading spaces trimmed
153 paragraph.push_back(line.substr(idx));
154 }
155 }
156
157 end_paragraph();
158
159 return tags;
160 }
161
162 } // namespace
163
Comment(const std::string & body)164 Comment::Comment(const std::string& body) : body(body) {
165 if (StartsWith(body, kLineCommentBegin)) {
166 type = Type::LINE;
167 } else if (StartsWith(body, kBlockCommentBegin) && EndsWith(body, kBlockCommentEnd)) {
168 type = Type::BLOCK;
169 } else {
170 AIDL_FATAL(AIDL_LOCATION_HERE) << "invalid comments body:" << body;
171 }
172 }
173
174 // Returns the immediate block comment from the list of comments.
175 // Only the last/block comment can have the tag.
176 //
177 // /* @hide */
178 // int x;
179 //
180 // But tags in line or distant comments don't count. In the following,
181 // the variable 'x' is not hidden.
182 //
183 // // @hide
184 // int x;
185 //
186 // /* @hide */
187 // /* this is the immemediate comment to 'x' */
188 // int x;
189 //
GetValidComment(const Comments & comments)190 static std::optional<Comment> GetValidComment(const Comments& comments) {
191 if (!comments.empty() && comments.back().type == Comment::Type::BLOCK) {
192 return comments.back();
193 }
194 return std::nullopt;
195 }
196
197 // Sees if comments have the @hide tag.
198 // Example: /** @hide */
HasHideInComments(const Comments & comments)199 bool HasHideInComments(const Comments& comments) {
200 const auto valid_comment = GetValidComment(comments);
201 return valid_comment && std::regex_search(valid_comment->body, kTagHideRegex);
202 }
203
204 // Finds the @deprecated tag in comments and returns it with optional note which
205 // follows the tag.
206 // Example: /** @deprecated reason */
FindDeprecated(const Comments & comments)207 std::optional<Deprecated> FindDeprecated(const Comments& comments) {
208 if (const auto valid_comment = GetValidComment(comments); valid_comment) {
209 for (const auto& [name, description] : BlockTags(comments.back())) {
210 // take the first @deprecated
211 if (kTagDeprecated == name) {
212 return Deprecated{description};
213 }
214 }
215 }
216 return std::nullopt;
217 }
218
219 // Formats comments for the Java backend.
220 // The last/block comment is transformed into javadoc(/** */)
221 // and others are used as they are.
FormatCommentsForJava(const Comments & comments)222 std::string FormatCommentsForJava(const Comments& comments) {
223 std::stringstream out;
224 for (auto it = begin(comments); it != end(comments); it++) {
225 const bool last = next(it) == end(comments);
226 auto lines = TrimmedLines(*it);
227
228 if (it->type == Comment::Type::LINE) {
229 for (const auto& line : lines) {
230 out << kLineCommentBegin << line;
231 }
232 } else {
233 if (last || StartsWith(it->body, kDocCommentBegin)) {
234 out << kDocCommentBegin;
235 } else {
236 out << kBlockCommentBegin;
237 }
238 bool multiline = lines.size() > 1;
239
240 if (multiline) out << "\n";
241 for (const auto& line : lines) {
242 if (multiline) out << kBlockCommentMid;
243 out << " " << line;
244 if (multiline) out << "\n";
245 }
246 out << " " << kBlockCommentEnd << "\n";
247 }
248 }
249 return out.str();
250 }
251
252 } // namespace aidl
253 } // namespace android
254