blob: 3ec806b881008bf8a5e70f9736f7a6c701bfa07c [file] [edit]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Generation of reference from protos
"use strict";
const protobufjs = require("protobufjs");
const fs = require("fs");
const path = require("path");
const argv = require("yargs").argv;
const PROJECT_ROOT = path.dirname(
path.dirname(path.dirname(path.dirname(__filename))),
);
const visited = {};
// This function is used to escape:
// - The message-level comment, which becomes a full paragraph.
// - The per-field comments, rendered as as table.
function escapeCommentCommon(comment) {
comment = comment || "";
// Remove Next id: NN lines.
comment = comment.replace(/(\n)?^\s*next.*\bid:.*$/gim, "");
// Hide our little dirty secrets.
comment = comment.replace(/(\n)?^\s*TODO\(\w+\):.*$/gim, "");
// Turn |variable| references into `variable`.
comment = comment.replace(/[|](\w+?)[|]/g, "`$1`");
return comment;
}
// This is used to escape only the per-field comments.
// Removes \n due to 80col wrapping and preserves only end-of-sentence line
// breaks.
function singleLineComment(comment) {
comment = escapeCommentCommon(comment);
comment = comment.trim();
comment = comment.replace(/([.:?!])\n/g, "$1<br>");
comment = comment.replace(/\n/g, " ");
return comment;
}
function getFullName(pType) {
let cur = pType;
let name = pType.name;
while (cur && cur.parent != cur && cur.parent instanceof protobufjs.Type) {
name = `${cur.parent.name}.${name}`;
cur = cur.parent;
}
return name;
}
function genType(pType, depth) {
depth = depth || 0;
console.assert(pType instanceof protobufjs.ReflectionObject);
const fullName = getFullName(pType);
if (fullName in visited) return "";
visited[fullName] = true;
const heading = "#" + "#".repeat(Math.min(depth, 2));
const anchor = depth > 0 ? `{#${fullName}} ` : "";
let md = `${heading} ${anchor}${fullName}`;
md += "\n";
const fileName = path.basename(pType.filename);
const relPath = path.relative(PROJECT_ROOT, pType.filename);
md += escapeCommentCommon(pType.comment);
md += `\n\nDefined in [${fileName}](/${relPath})\n\n`;
const subTypes = [];
if (pType instanceof protobufjs.Enum) {
md += "#### Enum values:\n";
md += "Name | Value | Description\n";
md += "---- | ----- | -----------\n";
for (const enumName of Object.keys(pType.values)) {
const enumVal = pType.values[enumName];
const comment = singleLineComment(pType.comments[enumName]);
md += `${enumName} | ${enumVal} | ${comment}\n`;
}
} else {
md += "#### Fields:\n";
md += "Field | Type | Description\n";
md += "----- | ---- | -----------\n";
for (const fieldName in pType.fields) {
const field = pType.fields[fieldName];
let type = field.type;
if (field.repeated) {
type = `${type}[]`;
}
if (field.resolvedType) {
// The TraceConfig proto is linked from the TracePacket reference.
// Instead of recursing and generating the TraceConfig types all over
// again, just link to the dedicated TraceConfig reference page.
if (getFullName(field.resolvedType) === "TraceConfig") {
type = `[${type}](/docs/reference/trace-config-proto.autogen)`;
} else {
subTypes.push(field.resolvedType);
type = `[${type}](#${getFullName(field.resolvedType)})`;
}
}
md += `${fieldName} | ${type} | ${singleLineComment(field.comment)}\n`;
}
}
md += "\n\n\n\n";
for (const subType of subTypes) md += genType(subType, depth + 1);
return md;
}
function main() {
const inProtoFile = argv["i"];
const protoName = argv["p"];
const outFile = argv["o"];
if (!inProtoFile || !protoName) {
console.error("Usage: -i input.proto -p protos.RootType [-o out.md]");
process.exit(1);
}
const parser = new protobufjs.Root();
parser.resolvePath = (_, target) => {
if (target == inProtoFile) {
// The root proto file passed from the cmdline will be relative to the
// root_build_dir (out/xxx) (e.g.: ../../protos/config)
return inProtoFile;
}
// All the other imports, instead, will be relative to the project root
// (e.g. protos/config/...)
return path.join(PROJECT_ROOT, target);
};
const cfg = parser.loadSync(inProtoFile, {
alternateCommentMode: true,
keepCase: true,
});
cfg.resolveAll();
const traceConfig = cfg.lookup(protoName);
const generatedMd = genType(traceConfig);
if (outFile) {
fs.writeFileSync(outFile, generatedMd);
} else {
console.log(generatedMd);
}
process.exit(0);
}
main();