// 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 <optional>
#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/span.h"

namespace google {
namespace protobuf {
namespace io {
namespace {
template <typename T>
std::optional<T> LookupInFrameStack(
    absl::string_view var,
    absl::Span<std::function<std::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 std::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;
    absl::string_view first_pp_directive;
    while (absl::ConsumePrefix(&format_string, "\n")) {
      // clang-format will think a # at the beginning of the line in a raw
      // string is a preprocessor directive and put it at the start of the line,
      // which throws off indent calculation. Skip past those to find code that
      // is indented more realistically.
      if (absl::StartsWith(format_string, "#")) {
        // We don't want to drop the first #... lines. We just want to skip
        // through here. Remember to allow resetting later.
        if (first_pp_directive.empty()) {
          first_pp_directive = format_string;
        }
        size_t next_newline_index = format_string.find('\n');
        if (next_newline_index != absl::string_view::npos) {
          format_string = format_string.substr(next_newline_index);
          continue;
        }
      }
      raw_string_indent = 0;
      format.is_raw_string = true;
      while (absl::ConsumePrefix(&format_string, " ")) {
        ++raw_string_indent;
      }
    }
    // Reset if we skipped through some #... lines, so that we don't drop them.
    if (!first_pp_directive.empty()) {
      format_string = first_pp_directive;
    }

    // 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()) {
          // If the first line is part of an ignored comment, consider that a
          // first line as well.
          is_first = false;
          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(const SourceLocation loc) {
  PrintOptions opts;
  opts.checks_are_debug_only = true;

  // Fills in the file name and line number if they are valid.
  const std::string source_location_str =
      loc.line() == 0 ? ""
                      : absl::StrCat(": ", loc.file_name(), ":", loc.line());
  if (!Validate(indent_ >= options_.spaces_per_indent, opts,
                absl::StrCat("Outdent() without matching Indent()",
                             source_location_str))) {
    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);
}

std::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 std::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 std::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,
                       std::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(std::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;
          }

          std::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;
      }

      std::optional<ValueView> sub;
      std::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
