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