1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "tools/gn/c_include_iterator.h"
6
7#include "base/logging.h"
8#include "base/strings/string_util.h"
9#include "tools/gn/input_file.h"
10#include "tools/gn/location.h"
11
12namespace {
13
14enum IncludeType {
15  INCLUDE_NONE,
16  INCLUDE_SYSTEM,  // #include <...>
17  INCLUDE_USER     // #include "..."
18};
19
20// Returns true if str starts with the prefix.
21bool StartsWith(const base::StringPiece& str, const base::StringPiece& prefix) {
22  base::StringPiece extracted = str.substr(0, prefix.size());
23  return extracted == prefix;
24}
25
26// Returns a new string piece referencing the same buffer as the argument, but
27// with leading space trimmed. This only checks for space and tab characters
28// since we're dealing with lines in C source files.
29base::StringPiece TrimLeadingWhitespace(const base::StringPiece& str) {
30  size_t new_begin = 0;
31  while (new_begin < str.size() &&
32         (str[new_begin] == ' ' || str[new_begin] == '\t'))
33    new_begin++;
34  return str.substr(new_begin);
35}
36
37// We don't want to count comment lines and preprocessor lines toward our
38// "max lines to look at before giving up" since the beginnings of some files
39// may have a lot of comments.
40//
41// We only handle C-style "//" comments since this is the normal commenting
42// style used in Chrome, and do so pretty stupidly. We don't want to write a
43// full C++ parser here, we're just trying to get a good heuristic for checking
44// the file.
45//
46// We assume the line has leading whitespace trimmed. We also assume that empty
47// lines have already been filtered out.
48bool ShouldCountTowardNonIncludeLines(const base::StringPiece& line) {
49  if (StartsWith(line, "//"))
50    return false;  // Don't count comments.
51  if (StartsWith(line, "#"))
52    return false;  // Don't count preprocessor.
53  if (base::ContainsOnlyChars(line, base::kWhitespaceASCII))
54    return false;  // Don't count whitespace lines.
55  return true;  // Count everything else.
56}
57
58// Given a line, checks to see if it looks like an include or import and
59// extract the path. The type of include is returned. Returns INCLUDE_NONE on
60// error or if this is not an include line.
61//
62// The 1-based character number on the line that the include was found at
63// will be filled into *begin_char.
64IncludeType ExtractInclude(const base::StringPiece& line,
65                           base::StringPiece* path,
66                           int* begin_char) {
67  static const char kInclude[] = "#include";
68  static const size_t kIncludeLen = arraysize(kInclude) - 1;  // No null.
69  static const char kImport[] = "#import";
70  static const size_t kImportLen = arraysize(kImport) - 1;  // No null.
71
72  base::StringPiece trimmed = TrimLeadingWhitespace(line);
73  if (trimmed.empty())
74    return INCLUDE_NONE;
75
76  base::StringPiece contents;
77  if (StartsWith(trimmed, base::StringPiece(kInclude, kIncludeLen)))
78    contents = TrimLeadingWhitespace(trimmed.substr(kIncludeLen));
79  else if (StartsWith(trimmed, base::StringPiece(kImport, kImportLen)))
80    contents = TrimLeadingWhitespace(trimmed.substr(kImportLen));
81
82  if (contents.empty())
83    return INCLUDE_NONE;
84
85  IncludeType type = INCLUDE_NONE;
86  char terminating_char = 0;
87  if (contents[0] == '"') {
88    type = INCLUDE_USER;
89    terminating_char = '"';
90  } else if (contents[0] == '<') {
91    type = INCLUDE_SYSTEM;
92    terminating_char = '>';
93  } else {
94    return INCLUDE_NONE;
95  }
96
97  // Count everything to next "/> as the contents.
98  size_t terminator_index = contents.find(terminating_char, 1);
99  if (terminator_index == base::StringPiece::npos)
100    return INCLUDE_NONE;
101
102  *path = contents.substr(1, terminator_index - 1);
103  // Note: one based so we do "+ 1".
104  *begin_char = static_cast<int>(path->data() - line.data()) + 1;
105  return type;
106}
107
108}  // namespace
109
110const int CIncludeIterator::kMaxNonIncludeLines = 10;
111
112CIncludeIterator::CIncludeIterator(const InputFile* input)
113    : input_file_(input),
114      file_(input->contents()),
115      offset_(0),
116      line_number_(0),
117      lines_since_last_include_(0) {
118}
119
120CIncludeIterator::~CIncludeIterator() {
121}
122
123bool CIncludeIterator::GetNextIncludeString(base::StringPiece* out,
124                                            LocationRange* location) {
125  base::StringPiece line;
126  int cur_line_number = 0;
127  while (lines_since_last_include_ <= kMaxNonIncludeLines &&
128         GetNextLine(&line, &cur_line_number)) {
129    base::StringPiece include_contents;
130    int begin_char;
131    IncludeType type = ExtractInclude(line, &include_contents, &begin_char);
132    if (type == INCLUDE_USER) {
133      // Only count user includes for now.
134      *out = include_contents;
135      *location = LocationRange(
136          Location(input_file_, cur_line_number, begin_char),
137          Location(input_file_, cur_line_number,
138                   begin_char + static_cast<int>(include_contents.size())));
139
140      lines_since_last_include_ = 0;
141      return true;
142    }
143
144    if (ShouldCountTowardNonIncludeLines(line))
145      lines_since_last_include_++;
146  }
147  return false;
148}
149
150bool CIncludeIterator::GetNextLine(base::StringPiece* line, int* line_number) {
151  if (offset_ == file_.size())
152    return false;
153
154  size_t begin = offset_;
155  while (offset_ < file_.size() && file_[offset_] != '\n')
156    offset_++;
157  line_number_++;
158
159  *line = file_.substr(begin, offset_ - begin);
160  *line_number = line_number_;
161
162  // If we didn't hit EOF, skip past the newline for the next one.
163  if (offset_ < file_.size())
164    offset_++;
165  return true;
166}
167