| // 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(); |