blob: f040dc1712bf9d4e1e031d2e93e41441bc76f7c9 [file]
<!--
Copyright 2016 The LUCI Authors. All rights reserved.
Use of this source code is governed under the Apache License, Version 2.0
that can be found in the LICENSE file.
-->
<!--
rpcExplorer.descUtil exposes helper methods to work with protobuf
descriptor messages.
Primarily it implements name and comments resolution in a FileDescriptorSet.
-->
<script>
'use strict';
var rpcExplorer = (function(rpcExplorer) {
rpcExplorer.descUtil = {
/**
* For all descriptors in the file annotate resolves
* SourceLocationInfo.Location message and assigns it to the
* sourceCodeInfo property of the descriptor.
* If fileSet is provided, for all field descriptors with non-empty type
* name, resolves the referenced type and assigns to "rpcExpTypeInfo"
* of the field descriptor.
* Prerequisite reading:
* https://github.com/luci/luci-go/blob/ea240d0/common/proto/google/descriptor/descriptor.proto#L713
* @param {FileDescriptorProto} file
* @param {FileDescriptorSet} fileSet
*/
annotate: function(file, fileSet) {
var self = this;
if (!file.sourceCodeInfo) {
return;
}
// First, build a map { path -> location message }.
function key(path) {
var key = '';
for (var i = 0; i < path.length; i++) {
key += path[i] + '.';
}
return key;
}
var locationMap = {};
for (var i = 0; i < file.sourceCodeInfo.location.length; i++) {
var loc = file.sourceCodeInfo.location[i];
if (loc.path) {
locationMap[key(loc.path)] = loc;
}
}
// Now join all descriptors in file with the map.
var path = [];
function annotateList(list, field, fn) {
if (!list) {
return;
}
path.push(field, 0);
for (var i = 0; i < list.length; i++) {
path[path.length - 1] = i;
list[i].sourceCodeInfo = locationMap[key(path)];
if (fn) {
fn(list[i]);
}
}
path.pop();
path.pop();
}
// The magic numbers below are message field numbers defined in
// descriptor.proto.
function annotateMessage(msg) {
annotateList(msg.field, 2);
annotateList(msg.nestedType, 3, annotateMessage);
annotateList(msg.enumType, 4, annotateEnum);
// Resolve referenced types.
if (!fileSet || !msg.field) {
return;
}
for (var i = 0; i < msg.field.length; i++) {
var field = msg.field[i];
if (field.typeName) {
field.rpcExpTypeInfo = self.resolve(
fileSet, self.trimPrefixDot(field.typeName));
}
}
}
function annotateEnum(e) {
annotateList(e.value, 2);
}
function annotateService(svc) {
annotateList(svc.method, 2);
}
annotateList(file.messageType, 4, annotateMessage);
annotateList(file.enumType, 5, annotateEnum);
annotateList(file.service, 6, annotateService);
},
/**
* Annotates a FileDescriptorSet.
*/
annotateSet: function(fileSet) {
for (var i = 0; i < fileSet.file.length; i++) {
this.annotate(fileSet.file[i], fileSet);
}
},
splitFullName: function(fullName) {
var lastDot = fullName.lastIndexOf('.');
if (lastDot === -1) {
return {
pkg: '',
name: fullName
};
}
return {
pkg: fullName.substr(0, lastDot),
name: fullName.substr(lastDot + 1)
};
},
/**
* Resolves services, methods, messages, fields, enums and enum values.
*/
resolve: function(desc, name) {
if (!desc || !name) {
return null;
}
name = this.splitFullName(name);
var self = this;
// searches in each list.
function checkLists(lists) {
if (!lists) {
return null;
}
for (var type in lists) {
var desc = self.findByName(lists[type], name.name);
if (desc) {
return {type: type, desc: desc };
}
}
return null;
}
// Check top-level descriptors.
for (var i = 0; i < desc.file.length; i++) {
var file = desc.file[i];
if (file['package'] != name.pkg) {
continue
}
var result = checkLists({
service: file.service,
messageType: file.messageType,
enumType: file.enumType
});
if (result) {
return result;
}
}
// Possibly the entity we are resolving is not top-level and
// name.name references an object inside an object referenced by
// name.pkg. Try to resolve name.pkg.
var parent = this.resolve(desc, name.pkg);
if (!parent) {
return null;
}
switch (parent.type) {
case 'service':
return checkLists({ method: parent.desc.method });
case 'messageType':
return checkLists({
field: parent.desc.field,
messageType: parent.desc.nestedType,
enumType: parent.desc.enumType
});
case 'enumType':
return checkLists({
enumValue: parent.desc.value
});
default:
return null;
}
},
findByName: function(array, name) {
if (!array) {
return null;
}
for (var i = 0; i < array.length; i++) {
if (array[i].name === name) {
return array[i];
}
}
return null;
},
findByJsonName: function(array, jsonName) {
if (!array) {
return null;
}
for (var i = 0; i < array.length; i++) {
if (array[i].jsonName === jsonName) {
return array[i];
}
}
return null;
},
trimPrefixDot: function(name) {
if (typeof name === 'string' && name.charAt(0) === '.') {
name = name.substr(1);
}
return name;
},
normalizeComment: function(comment, titleToStrip) {
if (!comment) {
return '';
}
// Skip any leading whitespace.
var i = 0;
while (i < comment.length && comment[i] == ' ') {
i++;
}
comment = comment.substr(i);
// Keep first paragraph only. A paragraph is delimited by '\n\n' or
// '. '.
var idx = comment.indexOf('\n\n');
if (idx == -1) {
idx = comment.indexOf('. ')
}
if (idx != -1) {
comment = comment.substr(0, idx);
}
// Strip leading title.
if (titleToStrip) {
var prefix = titleToStrip + ' ';
if (comment.substr(0, prefix.length) == prefix) {
comment = comment.substr(prefix.length);
}
}
return comment;
}
};
return rpcExplorer;
}(rpcExplorer || {}));
</script>