blob: 5c25c671d381bd2e356b51f509e557720ddffef4 [file] [log] [blame]
Jooyung Hand4fe00e2021-01-11 16:21:53 +09001/*
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 <string>
23#include <vector>
24
25#include "logging.h"
26
27using android::base::EndsWith;
28using android::base::Error;
29using android::base::Join;
30using android::base::Result;
31using android::base::Split;
32using android::base::StartsWith;
33using android::base::Trim;
34
35namespace android {
36namespace aidl {
37
38namespace {
39
40static const std::string_view kLineCommentBegin = "//";
41static const std::string_view kBlockCommentBegin = "/*";
42static const std::string_view kBlockCommentEnd = "*/";
43
44std::string ConsumePrefix(const std::string& s, std::string_view prefix) {
45 AIDL_FATAL_IF(!StartsWith(s, prefix), AIDL_LOCATION_HERE);
46 return s.substr(prefix.size());
47}
48
49std::string ConsumeSuffix(const std::string& s, std::string_view suffix) {
50 AIDL_FATAL_IF(!EndsWith(s, suffix), AIDL_LOCATION_HERE);
51 return s.substr(0, s.size() - suffix.size());
52}
53
54struct BlockTag {
55 std::string name;
56 std::string description;
57};
58
59struct Comments {
60 enum class Type { LINE, BLOCK };
61 Type type;
62 std::string body;
63
64 std::vector<std::string> TrimmedLines() const;
65 std::vector<BlockTag> BlockTags() const;
66};
67
68// Removes comment markers: //, /*, /**, */, optional leading "*" in doc/block comments
69// - keeps leading spaces, but trims trailing spaces
70// - keeps empty lines
71std::vector<std::string> Comments::TrimmedLines() const {
72 if (type == Type::LINE) return std::vector{ConsumePrefix(body, kLineCommentBegin)};
73
74 std::string stripped = ConsumeSuffix(ConsumePrefix(body, kBlockCommentBegin), kBlockCommentEnd);
75
76 std::vector<std::string> lines;
77 bool found_first_line = false;
78
79 for (auto& line : Split(stripped, "\n")) {
80 // Delete prefixes like " * ", " *", or " ".
81 size_t idx = 0;
82 for (; idx < line.size() && isspace(line[idx]); idx++)
83 ;
84 if (idx < line.size() && line[idx] == '*') idx++;
85 if (idx < line.size() && line[idx] == ' ') idx++;
86
87 const std::string& sanitized_line = line.substr(idx);
88 size_t i = sanitized_line.size();
89 for (; i > 0 && isspace(sanitized_line[i - 1]); i--)
90 ;
91
92 // Either the size is 0 or everything was whitespace.
93 bool is_empty_line = i == 0;
94
95 found_first_line = found_first_line || !is_empty_line;
96 if (!found_first_line) continue;
97
98 // if is_empty_line, i == 0 so substr == ""
99 lines.push_back(sanitized_line.substr(0, i));
100 }
101 // remove trailing empty lines
102 while (!lines.empty() && Trim(lines.back()).empty()) {
103 lines.pop_back();
104 }
105 return lines;
106}
107
108std::vector<BlockTag> Comments::BlockTags() const {
109 std::vector<BlockTag> tags;
110
111 // current tag and paragraph
112 std::string tag;
113 std::vector<std::string> paragraph;
114
115 auto end_paragraph = [&]() {
116 if (tag.empty()) {
117 paragraph.clear();
118 return;
119 }
120 // paragraph lines are trimed at both ends
121 tags.push_back({tag, Join(paragraph, " ")});
122 tag.clear();
123 paragraph.clear();
124 };
125
126 for (const auto& line : TrimmedLines()) {
127 size_t idx = 0;
128 // skip leading spaces
129 for (; idx < line.size() && isspace(line[idx]); idx++)
130 ;
131
132 if (idx == line.size()) {
133 // skip empty lines
134 } else if (line[idx] == '@') {
135 // end the current paragraph before reading a new block tag (+ description paragraph)
136 end_paragraph();
137
138 size_t end_idx = idx + 1;
139 for (; end_idx < line.size() && isalpha(line[end_idx]); end_idx++)
140 ;
141
142 tag = line.substr(idx, end_idx - idx);
143
144 if (end_idx < line.size() && line[end_idx] == ' ') end_idx++;
145 // skip empty line
146 if (end_idx < line.size()) {
147 paragraph.push_back(line.substr(end_idx));
148 }
149 } else {
150 // gather paragraph lines with leading spaces trimmed
151 paragraph.push_back(line.substr(idx));
152 }
153 }
154
155 end_paragraph();
156
157 return tags;
158}
159
160// TODO(b/177276676) remove this when comments are kept as parsed in AST
161Result<std::vector<Comments>> ParseComments(const std::string& comments) {
162 enum ParseState {
163 INITIAL,
164 SLASH,
165 SLASHSLASH,
166 SLASHSTAR,
167 STAR,
168 };
169 ParseState st = INITIAL;
170 std::string body;
171 std::vector<Comments> result;
172 for (const auto& c : comments) {
173 switch (st) {
174 case INITIAL: // trim ws & newlines
175 if (c == '/') {
176 st = SLASH;
177 body += c;
178 } else if (std::isspace(c)) {
179 // skip whitespaces outside comments
180 } else {
181 return Error() << "expecing / or space, but got unknown: " << c;
182 }
183 break;
184 case SLASH:
185 if (c == '/') {
186 st = SLASHSLASH;
187 body += c;
188 } else if (c == '*') {
189 st = SLASHSTAR;
190 body += c;
191 } else {
192 return Error() << "expecting / or *, but got unknown: " << c;
193 }
194 break;
195 case SLASHSLASH:
196 if (c == '\n') {
197 st = INITIAL;
198 result.push_back({Comments::Type::LINE, std::move(body)});
199 body.clear();
200 } else {
201 body += c;
202 }
203 break;
204 case SLASHSTAR:
205 body += c;
206 if (c == '*') {
207 st = STAR;
208 }
209 break;
210 case STAR: // read "*", about to close
211 body += c;
212 if (c == '/') { // close!
213 st = INITIAL;
214 result.push_back({Comments::Type::BLOCK, std::move(body)});
215 body.clear();
216 } else if (c == '*') {
217 // about to close...
218 } else {
219 st = SLASHSTAR;
220 }
221 break;
222 default:
223 return Error() << "unexpected state: " << st;
224 }
225 }
226 return result;
227}
228
229} // namespace
230
231// Finds @deprecated tag and returns it with optional note which follows the tag.
232std::optional<Deprecated> FindDeprecated(const std::string& comments) {
233 auto result = ParseComments(comments);
234 AIDL_FATAL_IF(!result.ok(), AIDL_LOCATION_HERE) << result.error();
235
236 const std::string kTagDeprecated = "@deprecated";
237 for (const auto& c : *result) {
238 for (const auto& [name, description] : c.BlockTags()) {
239 // take the first @deprecated
240 if (kTagDeprecated == name) {
241 return Deprecated{description};
242 }
243 }
244 }
245 return std::nullopt;
246}
247
248} // namespace aidl
249} // namespace android