xref: /aosp_15_r20/external/perfetto/infra/perfetto.dev/src/gen_proto_reference.js (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2020 The Android Open Source Project
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// Generation of reference from protos
16
17'use strict';
18
19const protobufjs = require('protobufjs');
20const fs = require('fs');
21const path = require('path');
22const argv = require('yargs').argv
23
24const PROJECT_ROOT =
25    path.dirname(path.dirname(path.dirname(path.dirname(__filename))));
26
27const visited = {};
28
29// This function is used to escape:
30// - The message-level comment, which becomes a full paragraph.
31// - The per-field comments, rendered as as table.
32function escapeCommentCommon(comment) {
33  comment = comment || '';
34
35  // Remove Next id: NN lines.
36  comment = comment.replace(/(\n)?^\s*next.*\bid:.*$/img, '');
37
38  // Hide our little dirty secrets.
39  comment = comment.replace(/(\n)?^\s*TODO\(\w+\):.*$/img, '');
40
41  // Turn |variable| references into `variable`.
42  comment = comment.replace(/[|](\w+?)[|]/g, '`$1`');
43  return comment;
44}
45
46// This is used to escape only the per-field comments.
47// Removes \n due to 80col wrapping and preserves only end-of-sentence line
48// breaks.
49function singleLineComment(comment) {
50  comment = escapeCommentCommon(comment);
51  comment = comment.trim();
52  comment = comment.replace(/([.:?!])\n/g, '$1<br>');
53  comment = comment.replace(/\n/g, ' ');
54  return comment;
55}
56
57function getFullName(pType) {
58  let cur = pType;
59  let name = pType.name;
60  while (cur && cur.parent != cur && cur.parent instanceof protobufjs.Type) {
61    name = `${cur.parent.name}.${name}`;
62    cur = cur.parent;
63  }
64  return name;
65}
66
67function genType(pType, depth) {
68  depth = depth || 0;
69  console.assert(pType instanceof protobufjs.ReflectionObject);
70  const fullName = getFullName(pType);
71  if (fullName in visited)
72    return '';
73  visited[fullName] = true;
74
75  const heading = '#' +
76      '#'.repeat(Math.min(depth, 2));
77  const anchor = depth > 0 ? `{#${fullName}} ` : '';
78  let md = `${heading} ${anchor}${fullName}`;
79  md += '\n';
80  const fileName = path.basename(pType.filename);
81  const relPath = path.relative(PROJECT_ROOT, pType.filename);
82
83  md += escapeCommentCommon(pType.comment);
84  md += `\n\nDefined in [${fileName}](/${relPath})\n\n`;
85
86  const subTypes = [];
87
88  if (pType instanceof protobufjs.Enum) {
89    md += '#### Enum values:\n';
90    md += 'Name | Value | Description\n';
91    md += '---- | ----- | -----------\n';
92    for (const enumName of Object.keys(pType.values)) {
93      const enumVal = pType.values[enumName];
94      const comment = singleLineComment(pType.comments[enumName]);
95      md += `${enumName} | ${enumVal} | ${comment}\n`
96    }
97  } else {
98    md += '#### Fields:\n';
99    md += 'Field | Type | Description\n';
100    md += '----- | ---- | -----------\n';
101
102    for (const fieldName in pType.fields) {
103      const field = pType.fields[fieldName];
104      let type = field.type;
105      if (field.repeated) {
106        type = `${type}[]`;
107      }
108      if (field.resolvedType) {
109        // The TraceConfig proto is linked from the TracePacket reference.
110        // Instead of recursing and generating the TraceConfig types all over
111        // again, just link to the dedicated TraceConfig reference page.
112        if (getFullName(field.resolvedType) === 'TraceConfig') {
113          type = `[${type}](/docs/reference/trace-config-proto.autogen)`;
114        } else {
115          subTypes.push(field.resolvedType);
116          type = `[${type}](#${getFullName(field.resolvedType)})`;
117        }
118      }
119      md += `${fieldName} | ${type} | ${singleLineComment(field.comment)}\n`
120    }
121  }
122  md += '\n\n\n\n';
123
124  for (const subType of subTypes)
125    md += genType(subType, depth + 1);
126
127  return md;
128}
129
130
131function main() {
132  const inProtoFile = argv['i'];
133  const protoName = argv['p'];
134  const outFile = argv['o'];
135  if (!inProtoFile || !protoName) {
136    console.error('Usage: -i input.proto -p protos.RootType [-o out.md]');
137    process.exit(1);
138  }
139
140  const parser = new protobufjs.Root();
141  parser.resolvePath = (_, target) => {
142    if (target == inProtoFile) {
143      // The root proto file passed from the cmdline will be relative to the
144      // root_build_dir (out/xxx) (e.g.: ../../protos/config)
145      return inProtoFile;
146    }
147    // All the other imports, instead, will be relative to the project root
148    // (e.g. protos/config/...)
149    return path.join(PROJECT_ROOT, target);
150  };
151
152  const cfg = parser.loadSync(
153      inProtoFile, {alternateCommentMode: true, keepCase: true});
154  cfg.resolveAll();
155  const traceConfig = cfg.lookup(protoName);
156  const generatedMd = genType(traceConfig);
157  if (outFile) {
158    fs.writeFileSync(outFile, generatedMd);
159  } else {
160    console.log(generatedMd);
161  }
162  process.exit(0);
163}
164
165main();
166