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