// 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:.*$/img, ''); // Hide our little dirty secrets. comment = comment.replace(/(\n)?^\s*TODO\(\w+\):.*$/img, ''); // 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
'); 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();