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 // Sees if comments have the @hide tag.
175 // Example: /** @hide */
HasHideInComments(const Comments & comments)176 bool HasHideInComments(const Comments& comments) {
177   for (const Comment& comment : comments) {
178     if (comment.type != Comment::Type::BLOCK) continue;
179     if (!std::regex_search(comment.body, kTagHideRegex)) continue;
180     return true;
181   }
182   return false;
183 }
184 
185 // Finds the @deprecated tag in comments and returns it with optional note which
186 // follows the tag.
187 // Example: /** @deprecated reason */
FindDeprecated(const Comments & comments)188 std::optional<Deprecated> FindDeprecated(const Comments& comments) {
189   for (const Comment& comment : comments) {
190     if (comment.type != Comment::Type::BLOCK) continue;
191 
192     for (const auto& [name, description] : BlockTags(comment)) {
193       // take the first @deprecated
194       if (kTagDeprecated == name) {
195         return Deprecated{description};
196       }
197     }
198   }
199   return std::nullopt;
200 }
201 
202 // Formats comments for the Java backend.
203 // The last/block comment is transformed into javadoc(/** */)
204 // and others are used as they are.
FormatCommentsForJava(const Comments & comments)205 std::string FormatCommentsForJava(const Comments& comments) {
206   std::stringstream out;
207   for (auto it = begin(comments); it != end(comments); it++) {
208     const bool last = next(it) == end(comments);
209     auto lines = TrimmedLines(*it);
210 
211     if (it->type == Comment::Type::LINE) {
212       for (const auto& line : lines) {
213         out << kLineCommentBegin << line;
214       }
215     } else {
216       if (last || StartsWith(it->body, kDocCommentBegin)) {
217         out << kDocCommentBegin;
218       } else {
219         out << kBlockCommentBegin;
220       }
221       bool multiline = lines.size() > 1;
222 
223       if (multiline) out << "\n";
224       for (const auto& line : lines) {
225         if (multiline) out << kBlockCommentMid;
226         out << " " << line;
227         if (multiline) out << "\n";
228       }
229       out << " " << kBlockCommentEnd << "\n";
230     }
231   }
232   return out.str();
233 }
234 
235 }  // namespace aidl
236 }  // namespace android
237