Emit deprecation note
@deprecated tag in comments can have optional note to explain the
rationale for deprecation and/or to suggest a replacing entity.
/** @deprecated use bar() */
void foo();
In this change, the deprecation note is emitted when specified.
For example, in C++, deprecation note is emitted as following:
void foo() __attribute__((deprecated("use bar()")) ...
Bug: 174514415
Test: aidl_unittests
Merged-In: I271af4d636e755a4d97f60d2daa6528acbe76a4e
Change-Id: I271af4d636e755a4d97f60d2daa6528acbe76a4e
(cherry picked from commit 6ec07c5482939803229445726044457383dbdaa9)
diff --git a/comments.cpp b/comments.cpp
new file mode 100644
index 0000000..5c25c67
--- /dev/null
+++ b/comments.cpp
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "comments.h"
+
+#include <android-base/result.h>
+#include <android-base/strings.h>
+
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "logging.h"
+
+using android::base::EndsWith;
+using android::base::Error;
+using android::base::Join;
+using android::base::Result;
+using android::base::Split;
+using android::base::StartsWith;
+using android::base::Trim;
+
+namespace android {
+namespace aidl {
+
+namespace {
+
+static const std::string_view kLineCommentBegin = "//";
+static const std::string_view kBlockCommentBegin = "/*";
+static const std::string_view kBlockCommentEnd = "*/";
+
+std::string ConsumePrefix(const std::string& s, std::string_view prefix) {
+ AIDL_FATAL_IF(!StartsWith(s, prefix), AIDL_LOCATION_HERE);
+ return s.substr(prefix.size());
+}
+
+std::string ConsumeSuffix(const std::string& s, std::string_view suffix) {
+ AIDL_FATAL_IF(!EndsWith(s, suffix), AIDL_LOCATION_HERE);
+ return s.substr(0, s.size() - suffix.size());
+}
+
+struct BlockTag {
+ std::string name;
+ std::string description;
+};
+
+struct Comments {
+ enum class Type { LINE, BLOCK };
+ Type type;
+ std::string body;
+
+ std::vector<std::string> TrimmedLines() const;
+ std::vector<BlockTag> BlockTags() const;
+};
+
+// Removes comment markers: //, /*, /**, */, optional leading "*" in doc/block comments
+// - keeps leading spaces, but trims trailing spaces
+// - keeps empty lines
+std::vector<std::string> Comments::TrimmedLines() const {
+ if (type == Type::LINE) return std::vector{ConsumePrefix(body, kLineCommentBegin)};
+
+ std::string stripped = ConsumeSuffix(ConsumePrefix(body, kBlockCommentBegin), kBlockCommentEnd);
+
+ std::vector<std::string> lines;
+ bool found_first_line = false;
+
+ for (auto& line : Split(stripped, "\n")) {
+ // Delete prefixes like " * ", " *", or " ".
+ size_t idx = 0;
+ for (; idx < line.size() && isspace(line[idx]); idx++)
+ ;
+ if (idx < line.size() && line[idx] == '*') idx++;
+ if (idx < line.size() && line[idx] == ' ') idx++;
+
+ const std::string& sanitized_line = line.substr(idx);
+ size_t i = sanitized_line.size();
+ for (; i > 0 && isspace(sanitized_line[i - 1]); i--)
+ ;
+
+ // Either the size is 0 or everything was whitespace.
+ bool is_empty_line = i == 0;
+
+ found_first_line = found_first_line || !is_empty_line;
+ if (!found_first_line) continue;
+
+ // if is_empty_line, i == 0 so substr == ""
+ lines.push_back(sanitized_line.substr(0, i));
+ }
+ // remove trailing empty lines
+ while (!lines.empty() && Trim(lines.back()).empty()) {
+ lines.pop_back();
+ }
+ return lines;
+}
+
+std::vector<BlockTag> Comments::BlockTags() const {
+ std::vector<BlockTag> tags;
+
+ // current tag and paragraph
+ std::string tag;
+ std::vector<std::string> paragraph;
+
+ auto end_paragraph = [&]() {
+ if (tag.empty()) {
+ paragraph.clear();
+ return;
+ }
+ // paragraph lines are trimed at both ends
+ tags.push_back({tag, Join(paragraph, " ")});
+ tag.clear();
+ paragraph.clear();
+ };
+
+ for (const auto& line : TrimmedLines()) {
+ size_t idx = 0;
+ // skip leading spaces
+ for (; idx < line.size() && isspace(line[idx]); idx++)
+ ;
+
+ if (idx == line.size()) {
+ // skip empty lines
+ } else if (line[idx] == '@') {
+ // end the current paragraph before reading a new block tag (+ description paragraph)
+ end_paragraph();
+
+ size_t end_idx = idx + 1;
+ for (; end_idx < line.size() && isalpha(line[end_idx]); end_idx++)
+ ;
+
+ tag = line.substr(idx, end_idx - idx);
+
+ if (end_idx < line.size() && line[end_idx] == ' ') end_idx++;
+ // skip empty line
+ if (end_idx < line.size()) {
+ paragraph.push_back(line.substr(end_idx));
+ }
+ } else {
+ // gather paragraph lines with leading spaces trimmed
+ paragraph.push_back(line.substr(idx));
+ }
+ }
+
+ end_paragraph();
+
+ return tags;
+}
+
+// TODO(b/177276676) remove this when comments are kept as parsed in AST
+Result<std::vector<Comments>> ParseComments(const std::string& comments) {
+ enum ParseState {
+ INITIAL,
+ SLASH,
+ SLASHSLASH,
+ SLASHSTAR,
+ STAR,
+ };
+ ParseState st = INITIAL;
+ std::string body;
+ std::vector<Comments> result;
+ for (const auto& c : comments) {
+ switch (st) {
+ case INITIAL: // trim ws & newlines
+ if (c == '/') {
+ st = SLASH;
+ body += c;
+ } else if (std::isspace(c)) {
+ // skip whitespaces outside comments
+ } else {
+ return Error() << "expecing / or space, but got unknown: " << c;
+ }
+ break;
+ case SLASH:
+ if (c == '/') {
+ st = SLASHSLASH;
+ body += c;
+ } else if (c == '*') {
+ st = SLASHSTAR;
+ body += c;
+ } else {
+ return Error() << "expecting / or *, but got unknown: " << c;
+ }
+ break;
+ case SLASHSLASH:
+ if (c == '\n') {
+ st = INITIAL;
+ result.push_back({Comments::Type::LINE, std::move(body)});
+ body.clear();
+ } else {
+ body += c;
+ }
+ break;
+ case SLASHSTAR:
+ body += c;
+ if (c == '*') {
+ st = STAR;
+ }
+ break;
+ case STAR: // read "*", about to close
+ body += c;
+ if (c == '/') { // close!
+ st = INITIAL;
+ result.push_back({Comments::Type::BLOCK, std::move(body)});
+ body.clear();
+ } else if (c == '*') {
+ // about to close...
+ } else {
+ st = SLASHSTAR;
+ }
+ break;
+ default:
+ return Error() << "unexpected state: " << st;
+ }
+ }
+ return result;
+}
+
+} // namespace
+
+// Finds @deprecated tag and returns it with optional note which follows the tag.
+std::optional<Deprecated> FindDeprecated(const std::string& comments) {
+ auto result = ParseComments(comments);
+ AIDL_FATAL_IF(!result.ok(), AIDL_LOCATION_HERE) << result.error();
+
+ const std::string kTagDeprecated = "@deprecated";
+ for (const auto& c : *result) {
+ for (const auto& [name, description] : c.BlockTags()) {
+ // take the first @deprecated
+ if (kTagDeprecated == name) {
+ return Deprecated{description};
+ }
+ }
+ }
+ return std::nullopt;
+}
+
+} // namespace aidl
+} // namespace android
\ No newline at end of file