| // Protocol Buffers - Google's data interchange format |
| // Copyright 2024 Google LLC. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd |
| |
| // Author: kenton@google.com (Kenton Varda) |
| // Based on original Protocol Buffers design by |
| // Sanjay Ghemawat, Jeff Dean, and others. |
| |
| #include "google/protobuf/io/printer.h" |
| |
| #include <stdlib.h> |
| |
| #include <cstddef> |
| #include <functional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/log/absl_check.h" |
| #include "absl/log/absl_log.h" |
| #include "absl/strings/ascii.h" |
| #include "absl/strings/escaping.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/str_split.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/strings/strip.h" |
| #include "absl/types/optional.h" |
| #include "absl/types/span.h" |
| #include "absl/types/variant.h" |
| |
| namespace google { |
| namespace protobuf { |
| namespace io { |
| namespace { |
| template <typename T> |
| absl::optional<T> LookupInFrameStack( |
| absl::string_view var, |
| absl::Span<std::function<absl::optional<T>(absl::string_view)>> frames) { |
| for (size_t i = frames.size(); i >= 1; --i) { |
| auto val = frames[i - 1](var); |
| if (val.has_value()) { |
| return val; |
| } |
| } |
| return absl::nullopt; |
| } |
| } // namespace |
| |
| struct Printer::Format { |
| struct Chunk { |
| // The chunk's text; if this is a variable, it does not include the $...$. |
| absl::string_view text; |
| |
| // Whether or not this is a variable name, i.e., a $...$. |
| bool is_var; |
| }; |
| |
| struct Line { |
| // Chunks to emit, split along $ and annotates as to whether it is a |
| // variable name. |
| std::vector<Chunk> chunks; |
| |
| // The indentation for this chunk. |
| size_t indent; |
| }; |
| |
| std::vector<Line> lines; |
| |
| // Whether this is a multiline raw string, according to internal heuristics. |
| bool is_raw_string = false; |
| }; |
| |
| Printer::Format Printer::TokenizeFormat(absl::string_view format_string, |
| const PrintOptions& options) { |
| Format format; |
| size_t raw_string_indent = 0; |
| if (options.strip_raw_string_indentation) { |
| // We are processing a call that looks like |
| // |
| // p->Emit(R"cc( |
| // class Foo { |
| // int x, y, z; |
| // }; |
| // )cc"); |
| // |
| // or |
| // |
| // p->Emit(R"cc( |
| // |
| // class Foo { |
| // int x, y, z; |
| // }; |
| // )cc"); |
| // |
| // To compute the indent, we need: |
| // 1. Iterate over each line. |
| // 2. Find the first line that contains non-whitespace characters. |
| // 3. Count the number of leading spaces on that line. |
| // |
| // The following pairs of loops assume the current line is the first line |
| // with non-whitespace characters; if we consume all the spaces and |
| // then immediately hit a newline, this means this wasn't the right line and |
| // we should start over. |
| // |
| // Note that the very first character *must* be a newline; that is how we |
| // detect that this is a multi-line raw string template, and as such this is |
| // a while loop, not a do/while loop. |
| |
| absl::string_view orig = format_string; |
| while (absl::ConsumePrefix(&format_string, "\n")) { |
| raw_string_indent = 0; |
| format.is_raw_string = true; |
| while (absl::ConsumePrefix(&format_string, " ")) { |
| ++raw_string_indent; |
| } |
| } |
| |
| // If we consume the entire string, this probably wasn't a raw string and |
| // was probably something like a couple of explicit newlines. |
| if (format_string.empty()) { |
| format_string = orig; |
| format.is_raw_string = false; |
| raw_string_indent = 0; |
| } |
| |
| // This means we have a preprocessor directive and we should not have eaten |
| // the newline. |
| if (!at_start_of_line_ && absl::StartsWith(format_string, "#")) { |
| format_string = orig; |
| } |
| } |
| |
| // We now split the remaining format string into lines and discard: |
| // 1. A trailing Printer-discarded comment, if this is a raw string. |
| // |
| // 2. All leading spaces to compute that line's indent. |
| // We do not do this for the first line, so that Emit(" ") works |
| // correctly. We do this *regardless* of whether we are processing |
| // a raw string, because existing non-raw-string calls to cpp::Formatter |
| // rely on this. There is a test that validates this behavior. |
| // |
| // 3. Set the indent for that line to max(0, line_indent - |
| // raw_string_indent), if this is not a raw string. |
| // |
| // 4. Trailing empty lines, if we know this is a raw string, except for |
| // a single extra newline at the end. |
| // |
| // Each line is itself split into chunks along the variable delimiters, e.g. |
| // $...$. |
| bool is_first = true; |
| for (absl::string_view line_text : absl::StrSplit(format_string, '\n')) { |
| if (format.is_raw_string) { |
| size_t comment_index = line_text.find(options_.ignored_comment_start); |
| if (comment_index != absl::string_view::npos) { |
| line_text = line_text.substr(0, comment_index); |
| if (absl::StripLeadingAsciiWhitespace(line_text).empty()) { |
| continue; |
| } |
| } |
| } |
| |
| size_t line_indent = 0; |
| while (!is_first && absl::ConsumePrefix(&line_text, " ")) { |
| ++line_indent; |
| } |
| is_first = false; |
| |
| format.lines.emplace_back(); |
| auto& line = format.lines.back(); |
| line.indent = |
| line_indent > raw_string_indent ? line_indent - raw_string_indent : 0; |
| |
| bool is_var = false; |
| size_t total_len = 0; |
| for (absl::string_view chunk : |
| absl::StrSplit(line_text, options_.variable_delimiter)) { |
| // The special _start and _end variables should actually glom the next |
| // chunk into themselves, so as to be of the form _start$foo and _end$foo. |
| if (!line.chunks.empty() && !is_var) { |
| auto& prev = line.chunks.back(); |
| if (prev.text == "_start" || prev.text == "_end") { |
| // The +1 below is to account for the $ in between them. |
| // This string is safe, because prev.text and chunk are contiguous |
| // by construction. |
| prev.text = absl::string_view(prev.text.data(), |
| prev.text.size() + 1 + chunk.size()); |
| |
| // Account for the foo$ part of $_start$foo$. |
| total_len += chunk.size() + 1; |
| continue; |
| } |
| } |
| |
| if (is_var || !chunk.empty()) { |
| line.chunks.push_back(Format::Chunk{chunk, is_var}); |
| } |
| |
| total_len += chunk.size(); |
| if (is_var) { |
| // This accounts for the $s around a variable. |
| total_len += 2; |
| } |
| |
| is_var = !is_var; |
| } |
| |
| // To ensure there are no unclosed $...$, we check that the computed length |
| // above equals the actual length of the string. If it's off, that means |
| // that there are missing or extra $ characters. |
| Validate(total_len == line_text.size(), options, [&line] { |
| if (line.chunks.empty()) { |
| return std::string("wrong number of variable delimiters"); |
| } |
| |
| return absl::StrFormat("unclosed variable name: `%s`", |
| absl::CHexEscape(line.chunks.back().text)); |
| }); |
| |
| // Trim any empty, non-variable chunks. |
| while (!line.chunks.empty()) { |
| auto& last = line.chunks.back(); |
| if (last.is_var || !last.text.empty()) { |
| break; |
| } |
| |
| line.chunks.pop_back(); |
| } |
| } |
| |
| // Discard any trailing newlines (i.e., lines which contain no chunks.) |
| if (format.is_raw_string) { |
| while (!format.lines.empty() && format.lines.back().chunks.empty()) { |
| format.lines.pop_back(); |
| } |
| } |
| |
| #if 0 // Use this to aid debugging tokenization. |
| LOG(INFO) << "--- " << format.lines.size() << " lines"; |
| for (size_t i = 0; i < format.lines.size(); ++i) { |
| const auto& line = format.lines[i]; |
| |
| auto log_line = absl::StrFormat("[\" \" x %d]", line.indent); |
| for (const auto& chunk : line.chunks) { |
| absl::StrAppendFormat(&log_line, " %s\"%s\"", chunk.is_var ? "$" : "", |
| absl::CHexEscape(chunk.text)); |
| } |
| LOG(INFO) << log_line; |
| } |
| LOG(INFO) << "---"; |
| #endif |
| |
| return format; |
| } |
| |
| constexpr absl::string_view Printer::kProtocCodegenTrace; |
| |
| Printer::Printer(ZeroCopyOutputStream* output) : Printer(output, Options{}) {} |
| |
| Printer::Printer(ZeroCopyOutputStream* output, Options options) |
| : sink_(output), options_(options) { |
| if (!options_.enable_codegen_trace.has_value()) { |
| // Trace-by-default is threaded through via an env var, rather than a |
| // global, so that child processes can pick it up as well. The flag |
| // --enable_codegen_trace setenv()'s this in protoc's startup code. |
| static const bool kEnableCodegenTrace = |
| ::getenv(kProtocCodegenTrace.data()) != nullptr; |
| options_.enable_codegen_trace = kEnableCodegenTrace; |
| } |
| } |
| |
| Printer::Printer(ZeroCopyOutputStream* output, char variable_delimiter, |
| AnnotationCollector* annotation_collector) |
| : Printer(output, Options{variable_delimiter, annotation_collector}) {} |
| |
| absl::string_view Printer::LookupVar(absl::string_view var) { |
| auto result = LookupInFrameStack(var, absl::MakeSpan(var_lookups_)); |
| ABSL_CHECK(result.has_value()) << "could not find " << var; |
| |
| auto* view = result->AsString(); |
| ABSL_CHECK(view != nullptr) |
| << "could not find " << var << "; found callback instead"; |
| |
| return *view; |
| } |
| |
| bool Printer::Validate(bool cond, Printer::PrintOptions opts, |
| absl::FunctionRef<std::string()> message) { |
| if (!cond) { |
| if (opts.checks_are_debug_only) { |
| ABSL_DLOG(FATAL) << message(); |
| } else { |
| ABSL_LOG(FATAL) << message(); |
| } |
| } |
| return cond; |
| } |
| |
| bool Printer::Validate(bool cond, Printer::PrintOptions opts, |
| absl::string_view message) { |
| return Validate(cond, opts, [=] { return std::string(message); }); |
| } |
| |
| // This function is outlined to isolate the use of |
| // ABSL_CHECK into the .cc file. |
| void Printer::Outdent() { |
| PrintOptions opts; |
| opts.checks_are_debug_only = true; |
| if (!Validate(indent_ >= options_.spaces_per_indent, opts, |
| "Outdent() without matching Indent()")) { |
| return; |
| } |
| indent_ -= options_.spaces_per_indent; |
| } |
| |
| void Printer::Emit(absl::Span<const Sub> vars, absl::string_view format, |
| SourceLocation loc) { |
| PrintOptions opts; |
| opts.strip_raw_string_indentation = true; |
| opts.loc = loc; |
| |
| auto defs = WithDefs(vars, /*allow_callbacks=*/true); |
| |
| PrintImpl(format, {}, opts); |
| } |
| |
| absl::optional<std::pair<size_t, size_t>> Printer::GetSubstitutionRange( |
| absl::string_view varname, PrintOptions opts) { |
| auto it = substitutions_.find(varname); |
| if (!Validate(it != substitutions_.end(), opts, [varname] { |
| return absl::StrCat("undefined variable in annotation: ", varname); |
| })) { |
| return absl::nullopt; |
| } |
| |
| std::pair<size_t, size_t> range = it->second; |
| if (!Validate(range.first <= range.second, opts, [range, varname] { |
| return absl::StrFormat( |
| "variable used for annotation used multiple times: %s (%d..%d)", |
| varname, range.first, range.second); |
| })) { |
| return absl::nullopt; |
| } |
| |
| return range; |
| } |
| |
| void Printer::Annotate(absl::string_view begin_varname, |
| absl::string_view end_varname, |
| absl::string_view file_path, |
| const std::vector<int>& path, |
| absl::optional<AnnotationCollector::Semantic> semantic) { |
| if (options_.annotation_collector == nullptr) { |
| return; |
| } |
| |
| PrintOptions opts; |
| opts.checks_are_debug_only = true; |
| auto begin = GetSubstitutionRange(begin_varname, opts); |
| auto end = GetSubstitutionRange(end_varname, opts); |
| if (!begin.has_value() || !end.has_value()) { |
| return; |
| } |
| if (begin->first > end->second) { |
| ABSL_DLOG(FATAL) << "annotation has negative length from " << begin_varname |
| << " to " << end_varname; |
| return; |
| } |
| options_.annotation_collector->AddAnnotation( |
| begin->first, end->second, std::string(file_path), path, semantic); |
| } |
| |
| void Printer::WriteRaw(const char* data, size_t size) { |
| if (failed_ || size == 0) { |
| return; |
| } |
| |
| if (at_start_of_line_ && data[0] != '\n') { |
| IndentIfAtStart(); |
| if (failed_) { |
| return; |
| } |
| |
| // Fix up empty variables (e.g., "{") that should be annotated as |
| // coming after the indent. |
| for (const std::string& var : line_start_variables_) { |
| auto& pair = substitutions_[var]; |
| pair.first += indent_; |
| pair.second += indent_; |
| } |
| } |
| |
| // If we're going to write any data, clear line_start_variables_, since |
| // we've either updated them in the block above or they no longer refer to |
| // the current line. |
| line_start_variables_.clear(); |
| |
| if (paren_depth_to_omit_.empty()) { |
| sink_.Append(data, size); |
| } else { |
| for (size_t i = 0; i < size; ++i) { |
| char c = data[i]; |
| switch (c) { |
| case '(': |
| paren_depth_++; |
| if (!paren_depth_to_omit_.empty() && |
| paren_depth_to_omit_.back() == paren_depth_) { |
| break; |
| } |
| |
| sink_.Append(&c, 1); |
| break; |
| case ')': |
| if (!paren_depth_to_omit_.empty() && |
| paren_depth_to_omit_.back() == paren_depth_) { |
| paren_depth_to_omit_.pop_back(); |
| paren_depth_--; |
| break; |
| } |
| |
| paren_depth_--; |
| sink_.Append(&c, 1); |
| break; |
| default: |
| sink_.Append(&c, 1); |
| break; |
| } |
| } |
| } |
| failed_ |= sink_.failed(); |
| } |
| |
| void Printer::IndentIfAtStart() { |
| if (!at_start_of_line_) { |
| return; |
| } |
| |
| for (size_t i = 0; i < indent_; ++i) { |
| sink_.Write(" "); |
| } |
| at_start_of_line_ = false; |
| } |
| |
| void Printer::PrintCodegenTrace(absl::optional<SourceLocation> loc) { |
| if (!options_.enable_codegen_trace.value_or(false) || !loc.has_value()) { |
| return; |
| } |
| |
| if (!at_start_of_line_) { |
| at_start_of_line_ = true; |
| line_start_variables_.clear(); |
| sink_.Write("\n"); |
| } |
| |
| PrintRaw(absl::StrFormat("%s @%s:%d\n", options_.comment_start, |
| loc->file_name(), loc->line())); |
| at_start_of_line_ = true; |
| } |
| |
| bool Printer::ValidateIndexLookupInBounds(size_t index, |
| size_t current_arg_index, |
| size_t args_len, PrintOptions opts) { |
| if (!Validate(index < args_len, opts, [this, index] { |
| return absl::StrFormat("annotation %c{%d%c is out of bounds", |
| options_.variable_delimiter, index + 1, |
| options_.variable_delimiter); |
| })) { |
| return false; |
| } |
| if (!Validate( |
| index <= current_arg_index, opts, [this, index, current_arg_index] { |
| return absl::StrFormat( |
| "annotation arg must be in correct order as given; expected " |
| "%c{%d%c but got %c{%d%c", |
| options_.variable_delimiter, current_arg_index + 1, |
| options_.variable_delimiter, options_.variable_delimiter, |
| index + 1, options_.variable_delimiter); |
| })) { |
| return false; |
| } |
| return true; |
| } |
| |
| void Printer::PrintImpl(absl::string_view format, |
| absl::Span<const std::string> args, PrintOptions opts) { |
| // Inside of this function, we set indentation as we print new lines from |
| // the format string. No matter how we exit this function, we should fix up |
| // the indent to what it was before we entered; a cleanup makes it easy to |
| // avoid this mistake. |
| size_t original_indent = indent_; |
| auto unindent = |
| absl::MakeCleanup([this, original_indent] { indent_ = original_indent; }); |
| |
| absl::string_view original = format; |
| |
| line_start_variables_.clear(); |
| |
| if (opts.use_substitution_map) { |
| substitutions_.clear(); |
| } |
| |
| auto fmt = TokenizeFormat(format, opts); |
| PrintCodegenTrace(opts.loc); |
| |
| size_t arg_index = 0; |
| bool skip_next_newline = false; |
| std::vector<AnnotationCollector::Annotation> annot_stack; |
| std::vector<std::pair<absl::string_view, size_t>> annot_records; |
| for (size_t line_idx = 0; line_idx < fmt.lines.size(); ++line_idx) { |
| const auto& line = fmt.lines[line_idx]; |
| |
| // We only print a newline for lines that follow the first; a loop iteration |
| // can also hint that we should not emit another newline through the |
| // `skip_next_newline` variable. |
| // |
| // We also assume that double newlines are undesirable, so we |
| // do not emit a newline if we are at the beginning of a line, *unless* the |
| // previous format line is actually empty. This behavior is specific to |
| // raw strings. |
| if (line_idx > 0) { |
| bool prev_was_empty = fmt.lines[line_idx - 1].chunks.empty(); |
| bool should_skip_newline = |
| skip_next_newline || |
| (fmt.is_raw_string && (at_start_of_line_ && !prev_was_empty)); |
| if (!should_skip_newline) { |
| line_start_variables_.clear(); |
| sink_.Write("\n"); |
| at_start_of_line_ = true; |
| } |
| } |
| skip_next_newline = false; |
| |
| indent_ = original_indent + line.indent; |
| |
| for (size_t chunk_idx = 0; chunk_idx < line.chunks.size(); ++chunk_idx) { |
| auto chunk = line.chunks[chunk_idx]; |
| |
| if (!chunk.is_var) { |
| PrintRaw(chunk.text); |
| continue; |
| } |
| |
| if (chunk.text.empty()) { |
| // `$$` is an escape for just `$`. |
| WriteRaw(&options_.variable_delimiter, 1); |
| continue; |
| } |
| |
| // If we get this far, we can conclude the chunk is a substitution |
| // variable; we rename the `chunk` variable to make this clear below. |
| absl::string_view var = chunk.text; |
| if (substitution_listener_ != nullptr) { |
| substitution_listener_(var, opts.loc.value_or(SourceLocation())); |
| } |
| if (opts.use_curly_brace_substitutions && |
| absl::ConsumePrefix(&var, "{")) { |
| if (!Validate(var.size() == 1u, opts, |
| "expected single-digit variable")) { |
| continue; |
| } |
| |
| if (!Validate(absl::ascii_isdigit(var[0]), opts, |
| "expected digit after {")) { |
| continue; |
| } |
| |
| size_t idx = var[0] - '1'; |
| if (!ValidateIndexLookupInBounds(idx, arg_index, args.size(), opts)) { |
| continue; |
| } |
| |
| if (idx == arg_index) { |
| ++arg_index; |
| } |
| |
| IndentIfAtStart(); |
| annot_stack.push_back({{sink_.bytes_written(), 0}, args[idx]}); |
| continue; |
| } |
| |
| if (opts.use_curly_brace_substitutions && |
| absl::ConsumePrefix(&var, "}")) { |
| // The rest of var is actually ignored, and this is apparently |
| // public API now. Oops? |
| if (!Validate(!annot_stack.empty(), opts, |
| "unexpected end of annotation")) { |
| continue; |
| } |
| |
| annot_stack.back().first.second = sink_.bytes_written(); |
| if (options_.annotation_collector != nullptr) { |
| options_.annotation_collector->AddAnnotationNew(annot_stack.back()); |
| } |
| annot_stack.pop_back(); |
| continue; |
| } |
| |
| absl::string_view prefix, suffix; |
| if (opts.strip_spaces_around_vars) { |
| var = absl::StripLeadingAsciiWhitespace(var); |
| prefix = chunk.text.substr(0, chunk.text.size() - var.size()); |
| var = absl::StripTrailingAsciiWhitespace(var); |
| suffix = chunk.text.substr(prefix.size() + var.size()); |
| } |
| |
| if (!Validate(!var.empty(), opts, "unexpected empty variable")) { |
| continue; |
| } |
| |
| bool is_start = absl::ConsumePrefix(&var, "_start$"); |
| bool is_end = absl::ConsumePrefix(&var, "_end$"); |
| if (opts.use_annotation_frames && (is_start || is_end)) { |
| if (is_start) { |
| IndentIfAtStart(); |
| annot_records.push_back({var, sink_.bytes_written()}); |
| |
| // Skip all whitespace immediately after a _start. |
| ++chunk_idx; |
| if (chunk_idx < line.chunks.size()) { |
| absl::string_view text = line.chunks[chunk_idx].text; |
| while (absl::ConsumePrefix(&text, " ")) { |
| } |
| PrintRaw(text); |
| } |
| } else { |
| // If a line consisted *only* of an _end, this will likely result in |
| // a blank line if we do not zap the newline after it, so we do that |
| // here. |
| if (line.chunks.size() == 1) { |
| skip_next_newline = true; |
| } |
| |
| auto record_var = annot_records.back(); |
| annot_records.pop_back(); |
| |
| if (!Validate(record_var.first == var, opts, [record_var, var] { |
| return absl::StrFormat( |
| "_start and _end variables must match, but got %s and %s, " |
| "respectively", |
| record_var.first, var); |
| })) { |
| continue; |
| } |
| |
| absl::optional<AnnotationRecord> record = |
| LookupInFrameStack(var, absl::MakeSpan(annotation_lookups_)); |
| |
| if (!Validate(record.has_value(), opts, [var] { |
| return absl::StrCat("undefined annotation variable: \"", |
| absl::CHexEscape(var), "\""); |
| })) { |
| continue; |
| } |
| |
| if (options_.annotation_collector != nullptr) { |
| options_.annotation_collector->AddAnnotation( |
| record_var.second, sink_.bytes_written(), record->file_path, |
| record->path, record->semantic); |
| } |
| } |
| |
| continue; |
| } |
| |
| absl::optional<ValueView> sub; |
| absl::optional<AnnotationRecord> same_name_record; |
| if (opts.allow_digit_substitutions && absl::ascii_isdigit(var[0])) { |
| if (!Validate(var.size() == 1u, opts, |
| "expected single-digit variable")) { |
| continue; |
| } |
| |
| size_t idx = var[0] - '1'; |
| if (!ValidateIndexLookupInBounds(idx, arg_index, args.size(), opts)) { |
| continue; |
| } |
| if (idx == arg_index) { |
| ++arg_index; |
| } |
| sub = args[idx]; |
| } else { |
| sub = LookupInFrameStack(var, absl::MakeSpan(var_lookups_)); |
| |
| if (opts.use_annotation_frames) { |
| same_name_record = |
| LookupInFrameStack(var, absl::MakeSpan(annotation_lookups_)); |
| } |
| } |
| |
| // By returning here in case of empty we also skip possible spaces inside |
| // the $...$, i.e. "void$ dllexpor$ f();" -> "void f();" in the empty |
| // case. |
| if (!Validate(sub.has_value(), opts, [var] { |
| return absl::StrCat("undefined variable: \"", absl::CHexEscape(var), |
| "\""); |
| })) { |
| continue; |
| } |
| |
| size_t range_start = sink_.bytes_written(); |
| size_t range_end = sink_.bytes_written(); |
| |
| if (const absl::string_view* str = sub->AsString()) { |
| if (at_start_of_line_ && str->empty()) { |
| line_start_variables_.emplace_back(var); |
| } |
| |
| if (!str->empty()) { |
| // If `sub` is empty, we do not print the spaces around it. |
| PrintRaw(prefix); |
| PrintRaw(*str); |
| range_end = sink_.bytes_written(); |
| range_start = range_end - str->size(); |
| PrintRaw(suffix); |
| } |
| } else { |
| const ValueView::Callback* fnc = sub->AsCallback(); |
| ABSL_CHECK(fnc != nullptr); |
| |
| Validate( |
| prefix.empty() && suffix.empty(), opts, |
| "substitution that resolves to callback cannot contain whitespace"); |
| |
| range_start = sink_.bytes_written(); |
| ABSL_CHECK((*fnc)()) |
| << "recursive call encountered while evaluating \"" << var << "\""; |
| range_end = sink_.bytes_written(); |
| } |
| |
| if (range_start == range_end && sub->consume_parens_if_empty) { |
| paren_depth_to_omit_.push_back(paren_depth_ + 1); |
| } |
| |
| // If we just evaluated a value which specifies end-of-line consume-after |
| // characters, and we're at the start of a line, that means we finished |
| // with a newline. |
| // |
| // We trim a single end-of-line `consume_after` character in this case. |
| // |
| // This helps callback formatting "work as expected" with respect to forms |
| // like |
| // |
| // class Foo { |
| // $methods$; |
| // }; |
| // |
| // Without this post-processing, it would turn into |
| // |
| // class Foo { |
| // void Bar() {}; |
| // }; |
| // |
| // in many cases. Without the `;`, clang-format may format the template |
| // incorrectly. |
| auto next_idx = chunk_idx + 1; |
| if (!sub->consume_after.empty() && next_idx < line.chunks.size() && |
| !line.chunks[next_idx].is_var) { |
| chunk_idx = next_idx; |
| |
| absl::string_view text = line.chunks[chunk_idx].text; |
| for (char c : sub->consume_after) { |
| if (absl::ConsumePrefix(&text, absl::string_view(&c, 1))) { |
| break; |
| } |
| } |
| |
| PrintRaw(text); |
| } |
| |
| if (same_name_record.has_value() && |
| options_.annotation_collector != nullptr) { |
| options_.annotation_collector->AddAnnotation( |
| range_start, range_end, same_name_record->file_path, |
| same_name_record->path, same_name_record->semantic); |
| } |
| |
| if (opts.use_substitution_map) { |
| auto insertion = |
| substitutions_.emplace(var, std::make_pair(range_start, range_end)); |
| |
| if (!insertion.second) { |
| // This variable was used multiple times. |
| // Make its span have negative length so |
| // we can detect it if it gets used in an |
| // annotation. |
| insertion.first->second = {1, 0}; |
| } |
| } |
| } |
| } |
| |
| Validate(arg_index == args.size(), opts, |
| [original] { return absl::StrCat("unused args: ", original); }); |
| Validate(annot_stack.empty(), opts, [this, original] { |
| return absl::StrFormat( |
| "annotation range was not closed; expected %c}%c: %s", |
| options_.variable_delimiter, options_.variable_delimiter, original); |
| }); |
| |
| // For multiline raw strings, we always make sure to end on a newline. |
| if (fmt.is_raw_string && !at_start_of_line_) { |
| PrintRaw("\n"); |
| at_start_of_line_ = true; |
| } |
| } |
| } // namespace io |
| } // namespace protobuf |
| } // namespace google |