blob: be4620e2bde42cfbdbbfa0089d47c95fd718bf7b [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "fidl/span_sequence.h"
#include <zircon/assert.h>
#include "fidl/raw_ast.h"
namespace fidl::fmt {
namespace {
const size_t kIndentation = 4;
const size_t kWrappedIndentation = kIndentation * 2;
// If the last printed entity was a StandaloneCommentSpanSequence, we need to make sure we print the
// leading_blank_lines_ of whatever SpanSequence we are currently looking at.
void MaybeAddBlankLinesAfterStandaloneComment(const SpanSequence* printing,
std::optional<SpanSequence::Kind> last_printed_kind,
std::string* out) {
if (printing->GetLeadingBlankLines() > 0 &&
last_printed_kind == SpanSequence::Kind::kStandaloneComment) {
while (!out->empty() && out->back() == ' ')
out->pop_back();
for (size_t i = 0; i < printing->GetLeadingBlankLines(); i++)
*out += "\n";
}
}
// Before printing some text after a newline, we want to make sure to indent to the proper position.
// No indentation is performed if we are not on a newline, or are on the first line of the output.
void MaybeIndentLine(size_t indentation, size_t outdentation, std::string* out) {
if (out->empty() || out->back() == '\n') {
// It may be tempting to write this as:
//
// *out += std::string(indentation > outdentation ? indentation - outdentation : 0, ' ');
//
// and omit the deleting while-clause below, but this will introduce a subtle bug. It is
// usually the case that the SpanSequence being outdented is actually the first child of the
// SpanSequence being indented, resulting in two calls like:
//
// MaybeIndentLine(N, 0, out); // For the parent.
// MaybeIndentLine(0, N, out); // For the first child.
//
// Using the formulation above, this would result in no outdentation being applied.
*out += std::string(indentation, ' ');
}
while (outdentation > 0 && !out->empty() && out->back() == ' ') {
out->pop_back();
outdentation -= 1;
}
}
// Walks a list of SpanSequences, returning the index of the first one that is not a comment.
std::optional<size_t> FirstNonCommentChildIndex(
const std::vector<std::unique_ptr<SpanSequence>>& list) {
for (size_t i = 0; i < list.size(); ++i) {
auto& item = list[i];
if (!item->IsComment()) {
return i;
}
}
return std::nullopt;
}
// Walks a list of SpanSequences, returning the index of the last one that is not a comment.
std::optional<size_t> LastNonCommentChildIndex(
const std::vector<std::unique_ptr<SpanSequence>>& list) {
for (size_t i = list.size(); i-- != 0;) {
auto& item = list[i];
if (!item->IsComment()) {
return i;
}
}
return std::nullopt;
}
} // namespace
void SpanSequence::Close() { closed_ = true; }
void TokenSpanSequence::Close() {
if (!IsClosed()) {
SetRequiredSize(span_.size());
SpanSequence::Close();
}
}
std::optional<SpanSequence::Kind> TokenSpanSequence::Print(
const size_t max_col_width, std::optional<SpanSequence::Kind> last_printed_kind,
size_t indentation, bool wrapped, AdjacentIndents adjacent_indents, std::string* out) const {
MaybeAddBlankLinesAfterStandaloneComment(this, last_printed_kind, out);
MaybeIndentLine(indentation, GetOutdentation(), out);
*out += std::string(span_);
return SpanSequence::Kind::kToken;
}
void CompositeSpanSequence::AddChild(std::unique_ptr<SpanSequence> child) {
ZX_ASSERT_MSG(!IsClosed(), "cannot AddChild to closed AtomicSpanSequence");
children_.push_back(std::move(child));
}
// Required size calculations take care to exclude comments, but to include all non-edge spaces, in
// their calculation. Thus, the string `foo bar` has an inline size of 7, the same as ` foo bar `.
// Additionally, this span, divided by a comment, has a required size of 7 as well:
//
// foo // comment
// bar
size_t CompositeSpanSequence::CalculateRequiredSize() const {
size_t required_size = 0;
const auto last = LastNonCommentChildIndex(children_);
for (size_t i = 0; i < children_.size(); i++) {
auto& child = children_[i];
switch (child->GetKind()) {
case SpanSequence::Kind::kMultiline: {
required_size += child->GetRequiredSize();
return required_size;
}
default: {
required_size += child->GetRequiredSize();
if (i < last.value_or(0) && child->HasTrailingSpace()) {
required_size += 1;
}
break;
}
}
}
return required_size;
}
void CompositeSpanSequence::Close() {
CloseChildren();
if (!IsClosed()) {
// If the first child is a token, delete its leading new lines value and hoist it up to the
// parent that is currently being closed. This prevents duplication of leading new lines, as
// the leading new new lines for a composite span are now always the parent's responsibility,
// not that of the first child.
auto first_non_comment_index = FirstNonCommentChildIndex(children_);
if (first_non_comment_index.has_value() && first_non_comment_index == 0) {
SetLeadingBlankLines(GetLeadingBlankLines() + children_[0]->GetLeadingBlankLines());
children_[0]->SetLeadingBlankLines(0);
}
// If the last child is a token with a trailing space, reset its trailing space boolean and
// set that of the parent (ie, the SpanSequence currently being closed). This prevents
// duplication of trailing spaces, as the trailing space for a composite span is now always the
// parent's responsibility, not that of the last child.
auto last_non_comment_index = LastNonCommentChildIndex(children_);
if (last_non_comment_index.has_value() && last_non_comment_index == children_.size() - 1 &&
children_[last_non_comment_index.value()]->HasTrailingSpace()) {
SetTrailingSpace(true);
children_[last_non_comment_index.value()]->SetTrailingSpace(false);
}
SetRequiredSize(CalculateRequiredSize());
SpanSequence::Close();
}
}
void CompositeSpanSequence::CloseChildren() {
if (!IsClosed()) {
for (size_t i = 0; i < children_.size(); ++i) {
const auto& child = children_[i];
if (!child->IsClosed())
child->Close();
switch (child->GetKind()) {
case SpanSequence::Kind::kToken: {
has_tokens_ = true;
break;
}
case SpanSequence::Kind::kInlineComment: {
has_non_leading_comments_ = true;
break;
}
case SpanSequence::Kind::kStandaloneComment: {
if (i > 0)
has_non_leading_comments_ = true;
break;
}
default: {
if (child->HasNonLeadingComments())
has_non_leading_comments_ = true;
if (child->HasTokens())
has_tokens_ = true;
}
}
}
}
}
SpanSequence* CompositeSpanSequence::GetLastChild() {
ZX_ASSERT_MSG(!IsClosed(), "cannot GetLastChild of closed AtomicSpanSequence");
const auto& children = GetChildren();
if (!children.empty()) {
return children.at(children.size() - 1).get();
}
return nullptr;
}
bool CompositeSpanSequence::IsEmpty() { return children_.empty(); }
std::vector<std::unique_ptr<SpanSequence>>& CompositeSpanSequence::EditChildren() {
return children_;
}
const std::vector<std::unique_ptr<SpanSequence>>& CompositeSpanSequence::GetChildren() const {
return children_;
}
std::optional<SpanSequence::Kind> AtomicSpanSequence::Print(
const size_t max_col_width, std::optional<SpanSequence::Kind> last_printed_kind,
size_t indentation, bool wrapped, AdjacentIndents adjacent_indents, std::string* out) const {
const auto& children = GetChildren();
const auto first = FirstNonCommentChildIndex(children);
const auto last = LastNonCommentChildIndex(children);
auto wrapped_indentation = indentation + (wrapped ? kWrappedIndentation : 0);
MaybeAddBlankLinesAfterStandaloneComment(this, last_printed_kind, out);
for (size_t i = 0; i < children.size(); ++i) {
const auto& child = children[i];
const AdjacentIndents indents = AdjacentIndents(
i == 0 && adjacent_indents.prev, i == children.size() - 1 && adjacent_indents.next);
switch (child->GetKind()) {
case SpanSequence::Kind::kAtomic:
case SpanSequence::Kind::kDivisible: {
MaybeIndentLine(wrapped_indentation, child->GetOutdentation(), out);
last_printed_kind =
child->Print(max_col_width, last_printed_kind, indentation, wrapped, indents, out);
// If the child AtomicSpanSequence had comments, we know that it forces a wrapping, so
// all future printing for this AtomicSpanSequence must be wrapped as well.
if (!wrapped && child->HasNonLeadingComments() && child->HasTokens()) {
wrapped = true;
wrapped_indentation += kWrappedIndentation;
}
break;
}
case SpanSequence::Kind::kToken: {
last_printed_kind = child->Print(max_col_width, last_printed_kind, wrapped_indentation,
wrapped, indents, out);
break;
}
case SpanSequence::Kind::kInlineComment: {
// An inline comment must always have a leading space, to properly separate it from the
// preceding token.
last_printed_kind =
child->Print(max_col_width, last_printed_kind, indentation, wrapped, indents, out);
// A comment always forces the rest of the AtomicSpanSequence content to be wrapped if its
// between non-comment children.
if (!wrapped && i >= first.value_or(children.size()) && i < last.value_or(0)) {
wrapped = true;
wrapped_indentation += kWrappedIndentation;
}
break;
}
case SpanSequence::Kind::kStandaloneComment: {
// Special case: If this is the very last child in the AtomicSpanSequence, and the next
// token after this AtomicSpanSequence will be indented, we want to make sure to indent this
// child StandaloneCommentSpanSequence as well. If we did not do this check, we would get
// something like the following (AtomicSpanSequence bounded with «», its children with ⸢⸥):
//
// type MyStruct = «⸢struct⸥ ⸢{⸥
// ⸢// You should have indented me!⸥»
// field1 ...
if (!wrapped && indents.HasAdjacentIndent()) {
indentation += kIndentation;
last_printed_kind =
child->Print(max_col_width, last_printed_kind, indentation, wrapped, indents, out);
break;
}
// A standalone comment always forces the rest of the AtomicSpanSequence content to be
// wrapped, unless that comment precedes the first non-comment token in the span.
if (!wrapped && i >= first.value_or(children.size())) {
wrapped = true;
wrapped_indentation += kWrappedIndentation;
}
last_printed_kind =
child->Print(max_col_width, last_printed_kind, indentation, wrapped, indents, out);
break;
}
case SpanSequence::Kind::kMultiline: {
MaybeIndentLine(wrapped_indentation, child->GetOutdentation(), out);
last_printed_kind =
child->Print(max_col_width, last_printed_kind, indentation, wrapped, indents, out);
if (!wrapped) {
wrapped = true;
}
break;
}
}
// If the last printed SpanSequence was a token, and that token has declared itself to have a
// trailing space, we print that space. However, if this is the last non-whitespace token in
// the current AtomicSpanSequence, this decision is delegated to its parent, so avoid printing
// for now.
if (child->HasTrailingSpace() && last_printed_kind == SpanSequence::Kind::kToken &&
i < last.value_or(0)) {
*out += " ";
}
}
return last_printed_kind;
}
std::optional<SpanSequence::Kind> DivisibleSpanSequence::Print(
const size_t max_col_width, std::optional<SpanSequence::Kind> last_printed_kind,
size_t indentation, bool wrapped, AdjacentIndents adjacent_indents, std::string* out) const {
const auto& children = GetChildren();
const auto required_size = GetRequiredSize();
const auto last = LastNonCommentChildIndex(children);
auto wrapped_indentation = indentation + (wrapped ? kWrappedIndentation : 0);
auto space_available = max_col_width - wrapped_indentation;
ZX_ASSERT_MSG(wrapped_indentation <= max_col_width, "indentation overflow");
MaybeAddBlankLinesAfterStandaloneComment(this, last_printed_kind, out);
// See the `NoPointlessWrapping` test case in `formatter_tests.cc` for an illustrative example
// of why the conditional after `&&` exists, but the brief explanation is that indenting in cases
// where there are only two elements in the DivisibleSpanSequence and the first one is very short
// (<8 chars) is counterproductive, producing output like:
//
// type MyStruct = {
// a
// MyVeryVery...VeryLongTypeName;
// };
if (required_size > space_available &&
(children.size() > 2 || children[0]->GetRequiredSize() >= kWrappedIndentation)) {
// We can't fit this DivisibleSpanSequence on a single line, either due to a lack of space, or
// otherwise because it has a MultiSpanSequence somewhere in the middle of its child nodes,
// which forces line breaks.
for (size_t i = 0; i < children.size(); ++i) {
const auto& child = children[i];
const AdjacentIndents indents = AdjacentIndents(
i == 0 && adjacent_indents.prev, i == children.size() - 1 && adjacent_indents.next);
MaybeIndentLine(wrapped_indentation, child->GetOutdentation(), out);
last_printed_kind =
child->Print(max_col_width, last_printed_kind, indentation, wrapped, indents, out);
if (i < last.value_or(0)) {
*out += "\n";
}
if (i == 0 && !wrapped) {
wrapped = true;
wrapped_indentation += kWrappedIndentation;
}
}
return last_printed_kind;
}
// We can fit this DivisibleSpanSequence on a single line!
for (size_t i = 0; i < children.size(); ++i) {
auto& child = children[i];
const AdjacentIndents indents = AdjacentIndents(
i == 0 && adjacent_indents.prev, i == children.size() - 1 && adjacent_indents.next);
switch (child->GetKind()) {
case SpanSequence::Kind::kInlineComment:
case SpanSequence::Kind::kStandaloneComment: {
ZX_PANIC("comments may not be children of DivisibleSpanSequence");
}
case SpanSequence::Kind::kAtomic:
case SpanSequence::Kind::kDivisible: {
MaybeIndentLine(wrapped_indentation, child->GetOutdentation(), out);
last_printed_kind =
child->Print(max_col_width, last_printed_kind, indentation, wrapped, indents, out);
// In certain weird circumstances (ie, comments placed in unexpected areas), a child
// AtomicSpanSequence may start with an inline comment. If this is the case, make sure to
// wrap the rest of this SpanSequence.
auto as_composite = static_cast<CompositeSpanSequence*>(child.get());
auto starts_with_inline = false;
if (!as_composite->IsEmpty()) {
const auto& inner_children = as_composite->GetChildren();
starts_with_inline = inner_children[0]->GetKind() == SpanSequence::Kind::kInlineComment;
}
// If the child AtomicSpanSequence had comments, we know that it forces a wrapping, so
// all future printing for this AtomicSpanSequence must be wrapped as well.
if (!wrapped && child->HasNonLeadingComments() &&
(child->HasTokens() || starts_with_inline)) {
wrapped = true;
wrapped_indentation += kWrappedIndentation;
}
break;
}
case SpanSequence::Kind::kToken: {
last_printed_kind = child->Print(max_col_width, last_printed_kind, indentation, wrapped,
adjacent_indents, out);
break;
}
case SpanSequence::Kind::kMultiline: {
MaybeIndentLine(wrapped_indentation, child->GetOutdentation(), out);
last_printed_kind = child->Print(max_col_width, last_printed_kind, indentation, wrapped,
adjacent_indents, out);
if (!wrapped) {
wrapped = true;
}
break;
}
}
// If the last printed SpanSequence was a token, and that token has declared itself to have a
// trailing space, we print that space. However, if this is the last non-whitespace token in
// the current AtomicSpanSequence, this decision is delegated to its parent, so avoid printing
// for now.
if (child->HasTrailingSpace() && last_printed_kind == SpanSequence::Kind::kToken &&
i < last.value_or(0)) {
*out += " ";
}
}
return last_printed_kind;
}
// For MultilineSpanSequences, we only require enough space on a given line to fit the first line of
// the SpanSequence, since the rest of it will be forced onto new lines anyway.
size_t MultilineSpanSequence::CalculateRequiredSize() const {
const auto& children = GetChildren();
const auto first = FirstNonCommentChildIndex(children);
if (first.has_value()) {
return children[first.value()]->GetRequiredSize();
}
return 0;
}
std::optional<SpanSequence::Kind> MultilineSpanSequence::Print(
const size_t max_col_width, std::optional<SpanSequence::Kind> last_printed_kind,
size_t indentation, bool wrapped, AdjacentIndents adjacent_indents, std::string* out) const {
const auto& children = GetChildren();
const auto first_non_comment_index = FirstNonCommentChildIndex(children);
for (size_t i = 0; i < children.size(); ++i) {
const auto& child = children[i];
auto child_indentation = indentation;
const bool prev_token_is_indented =
i == 0 ? adjacent_indents.prev
: (children[i - 1]->GetPosition() == SpanSequence::Position::kNewlineIndented &&
child->GetPosition() != SpanSequence::Position::kNewlineIndented);
const bool next_token_is_indented =
i == children.size() - 1
? adjacent_indents.next
: (children[i + 1]->GetPosition() == SpanSequence::Position::kNewlineIndented &&
child->GetPosition() != SpanSequence::Position::kNewlineIndented);
if (child->GetPosition() != SpanSequence::Position::kDefault) {
if (last_printed_kind == SpanSequence::Kind::kToken) {
// Omit one of the blank lines in cases where a MultilineSpanSequence is the first child of
// another MultilineSpanSequence, meaning that the first newline has already been printed.
auto blanks = child->GetLeadingBlankLines();
for (size_t n = !out->empty() && out->back() == '\n' ? 1 : 0; n <= blanks; n++) {
*out += "\n";
}
}
if (child->GetPosition() == SpanSequence::Position::kNewlineIndented) {
child_indentation += kIndentation;
}
if (child->GetKind() != SpanSequence::Kind::kMultiline) {
MaybeIndentLine(child_indentation, child->GetOutdentation(), out);
}
}
const bool keep_wrapped = wrapped && i == first_non_comment_index.value_or(children.size());
last_printed_kind =
child->Print(max_col_width, last_printed_kind, child_indentation, keep_wrapped,
AdjacentIndents(prev_token_is_indented, next_token_is_indented), out);
}
return last_printed_kind;
}
void CommentSpanSequence::Close() {
SetTrailingSpace(false);
SpanSequence::Close();
}
std::optional<SpanSequence::Kind> InlineCommentSpanSequence::Print(
const size_t max_col_width, std::optional<SpanSequence::Kind> last_printed_kind,
size_t indentation, bool wrapped, AdjacentIndents adjacent_indents, std::string* out) const {
// Remove all whitespace before the inline comment, then add exactly one space back.
while (!out->empty() && (out->back() == ' ' || out->back() == '\n'))
out->pop_back();
if (!out->empty())
*out += " ";
*out += std::string(comment_) + "\n";
return SpanSequence::Kind::kInlineComment;
}
// Consider this standalone comment:
//
// // line 1
// //
// // line 3
//
// // line 5
//
// Lines 1, 3, and 5 are stored in the "lines_" vector of StandaloneCommentSpanSequence as
// string_views like `// line N`. Line 2 is stored as `//`, while line 4 (technically totally
// absent, but still a connecting part of the comment block) is stored as an empty string_view.
void StandaloneCommentSpanSequence::AddLine(const std::string_view line,
size_t leading_blank_lines) {
ZX_ASSERT_MSG(!IsClosed(), "cannot AddLine to closed StandaloneCommentSpanSequence");
while (leading_blank_lines > 0) {
lines_.emplace_back(std::string_view());
leading_blank_lines--;
}
lines_.push_back(line);
}
std::optional<SpanSequence::Kind> StandaloneCommentSpanSequence::Print(
const size_t max_col_width, std::optional<SpanSequence::Kind> last_printed_kind,
size_t indentation, bool wrapped, AdjacentIndents adjacent_indents, std::string* out) const {
// A standalone comment forces a newline, but its possible that the preceding token already
// printed its trailing space(s), or otherwise we've already indented this line. We don't want to
// leave that trailing whitespace hanging before a newline, so let's delete the extra space(s).
// Deleting whitespace we've already printed is a bit clumsy, but this is the only place where we
// "undo" writes in this manner, and doing it this way allows us to keep the printer stateless.
while (!out->empty() && out->back() == ' ')
out->pop_back();
const auto wrapped_indentation = indentation + (wrapped ? kWrappedIndentation : 0);
if (last_printed_kind.has_value()) {
// Make sure we start the comment on a newline, if one has not already been added to the output.
if (last_printed_kind == SpanSequence::Kind::kToken && !out->empty() && out->back() != '\n') {
*out += "\n";
}
for (size_t i = 0; i < GetLeadingBlankLines(); i++) {
*out += "\n";
}
}
for (auto& line : lines_) {
if (!line.empty()) {
*out += std::string(wrapped_indentation, ' ');
*out += line;
}
*out += "\n";
}
return SpanSequence::Kind::kStandaloneComment;
}
} // namespace fidl::fmt