1 /* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14 ==============================================================================*/
15 #include "tensorflow/core/api_def/update_api_def.h"
16
17 #include <ctype.h>
18
19 #include <algorithm>
20 #include <string>
21 #include <vector>
22
23 #include "tensorflow/core/api_def/excluded_ops.h"
24 #include "tensorflow/core/framework/api_def.pb.h"
25 #include "tensorflow/core/framework/op.h"
26 #include "tensorflow/core/framework/op_def_builder.h"
27 #include "tensorflow/core/framework/op_gen_lib.h"
28 #include "tensorflow/core/lib/core/status.h"
29 #include "tensorflow/core/lib/io/path.h"
30 #include "tensorflow/core/lib/strings/stringprintf.h"
31 #include "tensorflow/core/platform/env.h"
32 #include "tensorflow/core/platform/protobuf.h"
33
34 namespace tensorflow {
35
36 namespace {
37 constexpr char kApiDefFileFormat[] = "api_def_%s.pbtxt";
38 // TODO(annarev): look into supporting other prefixes, not just 'doc'.
39 constexpr char kDocStart[] = ".Doc(R\"doc(";
40 constexpr char kDocEnd[] = ")doc\")";
41
42 // Updates api_def based on the given op.
FillBaseApiDef(ApiDef * api_def,const OpDef & op)43 void FillBaseApiDef(ApiDef* api_def, const OpDef& op) {
44 api_def->set_graph_op_name(op.name());
45 // Add arg docs
46 for (auto& input_arg : op.input_arg()) {
47 if (!input_arg.description().empty()) {
48 auto* api_def_in_arg = api_def->add_in_arg();
49 api_def_in_arg->set_name(input_arg.name());
50 api_def_in_arg->set_description(input_arg.description());
51 }
52 }
53 for (auto& output_arg : op.output_arg()) {
54 if (!output_arg.description().empty()) {
55 auto* api_def_out_arg = api_def->add_out_arg();
56 api_def_out_arg->set_name(output_arg.name());
57 api_def_out_arg->set_description(output_arg.description());
58 }
59 }
60 // Add attr docs
61 for (auto& attr : op.attr()) {
62 if (!attr.description().empty()) {
63 auto* api_def_attr = api_def->add_attr();
64 api_def_attr->set_name(attr.name());
65 api_def_attr->set_description(attr.description());
66 }
67 }
68 // Add docs
69 api_def->set_summary(op.summary());
70 api_def->set_description(op.description());
71 }
72
73 // Returns true if op has any description or summary.
OpHasDocs(const OpDef & op)74 bool OpHasDocs(const OpDef& op) {
75 if (!op.summary().empty() || !op.description().empty()) {
76 return true;
77 }
78 for (const auto& arg : op.input_arg()) {
79 if (!arg.description().empty()) {
80 return true;
81 }
82 }
83 for (const auto& arg : op.output_arg()) {
84 if (!arg.description().empty()) {
85 return true;
86 }
87 }
88 for (const auto& attr : op.attr()) {
89 if (!attr.description().empty()) {
90 return true;
91 }
92 }
93 return false;
94 }
95
96 // Returns true if summary and all descriptions are the same in op1
97 // and op2.
CheckDocsMatch(const OpDef & op1,const OpDef & op2)98 bool CheckDocsMatch(const OpDef& op1, const OpDef& op2) {
99 if (op1.summary() != op2.summary() ||
100 op1.description() != op2.description() ||
101 op1.input_arg_size() != op2.input_arg_size() ||
102 op1.output_arg_size() != op2.output_arg_size() ||
103 op1.attr_size() != op2.attr_size()) {
104 return false;
105 }
106 // Iterate over args and attrs to compare their docs.
107 for (int i = 0; i < op1.input_arg_size(); ++i) {
108 if (op1.input_arg(i).description() != op2.input_arg(i).description()) {
109 return false;
110 }
111 }
112 for (int i = 0; i < op1.output_arg_size(); ++i) {
113 if (op1.output_arg(i).description() != op2.output_arg(i).description()) {
114 return false;
115 }
116 }
117 for (int i = 0; i < op1.attr_size(); ++i) {
118 if (op1.attr(i).description() != op2.attr(i).description()) {
119 return false;
120 }
121 }
122 return true;
123 }
124
125 // Returns true if descriptions and summaries in op match a
126 // given single doc-string.
ValidateOpDocs(const OpDef & op,const string & doc)127 bool ValidateOpDocs(const OpDef& op, const string& doc) {
128 OpDefBuilder b(op.name());
129 // We don't really care about type we use for arguments and
130 // attributes. We just want to make sure attribute and argument names
131 // are added so that descriptions can be assigned to them when parsing
132 // documentation.
133 for (const auto& arg : op.input_arg()) {
134 b.Input(arg.name() + ":string");
135 }
136 for (const auto& arg : op.output_arg()) {
137 b.Output(arg.name() + ":string");
138 }
139 for (const auto& attr : op.attr()) {
140 b.Attr(attr.name() + ":string");
141 }
142 b.Doc(doc);
143 OpRegistrationData op_reg_data;
144 TF_CHECK_OK(b.Finalize(&op_reg_data));
145 return CheckDocsMatch(op, op_reg_data.op_def);
146 }
147 } // namespace
148
RemoveDoc(const OpDef & op,const string & file_contents,size_t start_location)149 string RemoveDoc(const OpDef& op, const string& file_contents,
150 size_t start_location) {
151 // Look for a line starting with .Doc( after the REGISTER_OP.
152 const auto doc_start_location = file_contents.find(kDocStart, start_location);
153 const string format_error = strings::Printf(
154 "Could not find %s doc for removal. Make sure the doc is defined with "
155 "'%s' prefix and '%s' suffix or remove the doc manually.",
156 op.name().c_str(), kDocStart, kDocEnd);
157 if (doc_start_location == string::npos) {
158 std::cerr << format_error << std::endl;
159 LOG(ERROR) << "Didn't find doc start";
160 return file_contents;
161 }
162 const auto doc_end_location = file_contents.find(kDocEnd, doc_start_location);
163 if (doc_end_location == string::npos) {
164 LOG(ERROR) << "Didn't find doc start";
165 std::cerr << format_error << std::endl;
166 return file_contents;
167 }
168
169 const auto doc_start_size = sizeof(kDocStart) - 1;
170 string doc_text = file_contents.substr(
171 doc_start_location + doc_start_size,
172 doc_end_location - doc_start_location - doc_start_size);
173
174 // Make sure the doc text we found actually matches OpDef docs to
175 // avoid removing incorrect text.
176 if (!ValidateOpDocs(op, doc_text)) {
177 LOG(ERROR) << "Invalid doc: " << doc_text;
178 std::cerr << format_error << std::endl;
179 return file_contents;
180 }
181 // Remove .Doc call.
182 auto before_doc = file_contents.substr(0, doc_start_location);
183 absl::StripTrailingAsciiWhitespace(&before_doc);
184 return before_doc +
185 file_contents.substr(doc_end_location + sizeof(kDocEnd) - 1);
186 }
187
188 namespace {
189 // Remove .Doc calls that follow REGISTER_OP calls for the given ops.
190 // We search for REGISTER_OP calls in the given op_files list.
RemoveDocs(const std::vector<const OpDef * > & ops,const std::vector<string> & op_files)191 void RemoveDocs(const std::vector<const OpDef*>& ops,
192 const std::vector<string>& op_files) {
193 // Set of ops that we already found REGISTER_OP calls for.
194 std::set<string> processed_ops;
195
196 for (const auto& file : op_files) {
197 string file_contents;
198 bool file_contents_updated = false;
199 TF_CHECK_OK(ReadFileToString(Env::Default(), file, &file_contents));
200
201 for (auto op : ops) {
202 if (processed_ops.find(op->name()) != processed_ops.end()) {
203 // We already found REGISTER_OP call for this op in another file.
204 continue;
205 }
206 string register_call =
207 strings::Printf("REGISTER_OP(\"%s\")", op->name().c_str());
208 const auto register_call_location = file_contents.find(register_call);
209 // Find REGISTER_OP(OpName) call.
210 if (register_call_location == string::npos) {
211 continue;
212 }
213 std::cout << "Removing .Doc call for " << op->name() << " from " << file
214 << "." << std::endl;
215 file_contents = RemoveDoc(*op, file_contents, register_call_location);
216 file_contents_updated = true;
217
218 processed_ops.insert(op->name());
219 }
220 if (file_contents_updated) {
221 TF_CHECK_OK(WriteStringToFile(Env::Default(), file, file_contents))
222 << "Could not remove .Doc calls in " << file
223 << ". Make sure the file is writable.";
224 }
225 }
226 }
227 } // namespace
228
229 // Returns ApiDefs text representation in multi-line format
230 // constructed based on the given op.
CreateApiDef(const OpDef & op)231 string CreateApiDef(const OpDef& op) {
232 ApiDefs api_defs;
233 FillBaseApiDef(api_defs.add_op(), op);
234
235 const std::vector<string> multi_line_fields = {"description"};
236 std::string new_api_defs_str;
237 ::tensorflow::protobuf::TextFormat::PrintToString(api_defs,
238 &new_api_defs_str);
239 return PBTxtToMultiline(new_api_defs_str, multi_line_fields);
240 }
241
242 // Creates ApiDef files for any new ops.
243 // If op_file_pattern is not empty, then also removes .Doc calls from
244 // new op registrations in these files.
CreateApiDefs(const OpList & ops,const string & api_def_dir,const string & op_file_pattern)245 void CreateApiDefs(const OpList& ops, const string& api_def_dir,
246 const string& op_file_pattern) {
247 auto* excluded_ops = GetExcludedOps();
248 std::vector<const OpDef*> new_ops_with_docs;
249
250 for (const auto& op : ops.op()) {
251 if (excluded_ops->find(op.name()) != excluded_ops->end()) {
252 continue;
253 }
254 // Form the expected ApiDef path.
255 string file_path =
256 io::JoinPath(tensorflow::string(api_def_dir), kApiDefFileFormat);
257 file_path = strings::Printf(file_path.c_str(), op.name().c_str());
258
259 // Create ApiDef if it doesn't exist.
260 if (!Env::Default()->FileExists(file_path).ok()) {
261 std::cout << "Creating ApiDef file " << file_path << std::endl;
262 const auto& api_def_text = CreateApiDef(op);
263 TF_CHECK_OK(WriteStringToFile(Env::Default(), file_path, api_def_text));
264
265 if (OpHasDocs(op)) {
266 new_ops_with_docs.push_back(&op);
267 }
268 }
269 }
270 if (!op_file_pattern.empty()) {
271 std::vector<string> op_files;
272 TF_CHECK_OK(Env::Default()->GetMatchingPaths(op_file_pattern, &op_files));
273 RemoveDocs(new_ops_with_docs, op_files);
274 }
275 }
276 } // namespace tensorflow
277