1 // Copyright 2020 Google LLC
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 package com.google.api.generator.gapic.protoparser;
16 
17 import com.google.api.generator.gapic.model.SourceCodeInfoLocation;
18 import com.google.common.base.Function;
19 import com.google.common.base.Joiner;
20 import com.google.common.collect.ImmutableList;
21 import com.google.common.collect.ImmutableListMultimap;
22 import com.google.common.collect.Maps;
23 import com.google.common.collect.Multimaps;
24 import com.google.protobuf.DescriptorProtos.DescriptorProto;
25 import com.google.protobuf.DescriptorProtos.EnumDescriptorProto;
26 import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
27 import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto;
28 import com.google.protobuf.DescriptorProtos.SourceCodeInfo.Location;
29 import com.google.protobuf.Descriptors.Descriptor;
30 import com.google.protobuf.Descriptors.EnumDescriptor;
31 import com.google.protobuf.Descriptors.EnumValueDescriptor;
32 import com.google.protobuf.Descriptors.FieldDescriptor;
33 import com.google.protobuf.Descriptors.FileDescriptor;
34 import com.google.protobuf.Descriptors.MethodDescriptor;
35 import com.google.protobuf.Descriptors.OneofDescriptor;
36 import com.google.protobuf.Descriptors.ServiceDescriptor;
37 import java.util.Map;
38 import javax.annotation.Nullable;
39 
40 /**
41  * A helper class which provides protocol buffer source info for descriptors.
42  *
43  * <p>In order to make this work, the descriptors need to be produced using the flag {@code
44  * --include_source_info}. Note that descriptors taken from the generated java code have source info
45  * stripped, and won't work with this class.
46  *
47  * <p>This class uses internal caches to speed up access to the source info. It is not thread safe.
48  * If you think you need this functionality in a thread-safe context, feel free to suggest a
49  * refactor.
50  */
51 public class SourceCodeInfoParser {
52   /**
53    * A map from file descriptors to the analyzed source info, stored as a multimap from a path of
54    * the form {@code n.m.l} to the location info.
55    */
56   private final Map<FileDescriptor, ImmutableListMultimap<String, Location>> fileToPathToLocation =
57       Maps.newHashMap();
58 
59   /** A map from descriptor objects to the path to those objects in their proto file. */
60   private final Map<Object, String> descriptorToPath = Maps.newHashMap();
61 
62   /** Gets the location of a message, if available. */
63   @Nullable
getLocation(Descriptor message)64   public SourceCodeInfoLocation getLocation(Descriptor message) {
65     FileDescriptor file = message.getFile();
66     if (!file.toProto().hasSourceCodeInfo()) {
67       return null;
68     }
69     return SourceCodeInfoLocation.create(getLocation(file, buildPath(message)));
70   }
71 
72   /** Gets the location of a field, if available. */
73   @Nullable
getLocation(FieldDescriptor field)74   public SourceCodeInfoLocation getLocation(FieldDescriptor field) {
75     FileDescriptor file = field.getFile();
76     if (!file.toProto().hasSourceCodeInfo()) {
77       return null;
78     }
79     Location fieldLocation = getLocation(file, buildPath(field));
80     return SourceCodeInfoLocation.create(fieldLocation);
81   }
82 
83   /** Gets the location of a service, if available. */
84   @Nullable
getLocation(ServiceDescriptor service)85   public SourceCodeInfoLocation getLocation(ServiceDescriptor service) {
86     FileDescriptor file = service.getFile();
87     if (!file.toProto().hasSourceCodeInfo()) {
88       return null;
89     }
90     return SourceCodeInfoLocation.create(getLocation(file, buildPath(service)));
91   }
92 
93   /** Gets the location of a method, if available. */
94   @Nullable
getLocation(MethodDescriptor method)95   public SourceCodeInfoLocation getLocation(MethodDescriptor method) {
96     FileDescriptor file = method.getFile();
97     if (!file.toProto().hasSourceCodeInfo()) {
98       return null;
99     }
100     return SourceCodeInfoLocation.create(getLocation(file, buildPath(method)));
101   }
102 
103   /** Gets the location of an enum type, if available. */
104   @Nullable
getLocation(EnumDescriptor enumType)105   public SourceCodeInfoLocation getLocation(EnumDescriptor enumType) {
106     FileDescriptor file = enumType.getFile();
107     if (!file.toProto().hasSourceCodeInfo()) {
108       return null;
109     }
110     return SourceCodeInfoLocation.create(getLocation(file, buildPath(enumType)));
111   }
112 
113   /** Gets the location of an enum value, if available. */
114   @Nullable
getLocation(EnumValueDescriptor enumValue)115   public SourceCodeInfoLocation getLocation(EnumValueDescriptor enumValue) {
116     FileDescriptor file = enumValue.getFile();
117     if (!file.toProto().hasSourceCodeInfo()) {
118       return null;
119     }
120     return SourceCodeInfoLocation.create(getLocation(file, buildPath(enumValue)));
121   }
122 
123   /** Gets the location of a oneof, if available. */
124   @Nullable
getLocation(OneofDescriptor oneof)125   public SourceCodeInfoLocation getLocation(OneofDescriptor oneof) {
126     FileDescriptor file = oneof.getFile();
127     if (!file.toProto().hasSourceCodeInfo()) {
128       return null;
129     }
130     return SourceCodeInfoLocation.create(getLocation(file, buildPath(oneof)));
131   }
132 
133   // -----------------------------------------------------------------------------
134   // Helpers
135 
136   /**
137    * A helper to compute the location based on a file descriptor and a path into that descriptor.
138    */
getLocation(FileDescriptor file, String path)139   private Location getLocation(FileDescriptor file, String path) {
140     ImmutableList<Location> cands = getCandidateLocations(file, path);
141     if (cands != null && cands.isEmpty()) {
142       return null;
143     } else {
144       return cands.get(0); // We choose the first one.
145     }
146   }
147 
getCandidateLocations(FileDescriptor file, String path)148   private ImmutableList<Location> getCandidateLocations(FileDescriptor file, String path) {
149     ImmutableListMultimap<String, Location> locationMap = fileToPathToLocation.get(file);
150     if (locationMap == null) {
151       locationMap =
152           Multimaps.index(
153               file.toProto().getSourceCodeInfo().getLocationList(),
154               new Function<Location, String>() {
155                 @Override
156                 public String apply(Location location) {
157                   return Joiner.on('.').join(location.getPathList());
158                 }
159               });
160       fileToPathToLocation.put(file, locationMap);
161     }
162     return locationMap.get(path);
163   }
164 
buildPath(Descriptor message)165   private String buildPath(Descriptor message) {
166     String path = descriptorToPath.get(message);
167     if (path != null) {
168       return path;
169     }
170     if (message.getContainingType() != null) {
171       path =
172           String.format(
173               "%s.%d.%d",
174               buildPath(message.getContainingType()),
175               DescriptorProto.NESTED_TYPE_FIELD_NUMBER,
176               message.getContainingType().getNestedTypes().indexOf(message));
177     } else {
178       path =
179           String.format(
180               "%d.%d",
181               FileDescriptorProto.MESSAGE_TYPE_FIELD_NUMBER,
182               message.getFile().getMessageTypes().indexOf(message));
183     }
184     descriptorToPath.put(message, path);
185     return path;
186   }
187 
buildPath(FieldDescriptor field)188   private String buildPath(FieldDescriptor field) {
189     String path = descriptorToPath.get(field);
190     if (path != null) {
191       return path;
192     }
193     if (field.isExtension()) {
194       if (field.getExtensionScope() == null) {
195         path =
196             String.format(
197                 "%d.%d",
198                 FileDescriptorProto.EXTENSION_FIELD_NUMBER,
199                 field.getFile().getExtensions().indexOf(field));
200       } else {
201         path =
202             String.format(
203                 "%s.%d.%d",
204                 buildPath(field.getExtensionScope()),
205                 DescriptorProto.EXTENSION_FIELD_NUMBER,
206                 field.getExtensionScope().getExtensions().indexOf(field));
207       }
208     } else {
209       path =
210           String.format(
211               "%s.%d.%d",
212               buildPath(field.getContainingType()),
213               DescriptorProto.FIELD_FIELD_NUMBER,
214               field.getContainingType().getFields().indexOf(field));
215     }
216     descriptorToPath.put(field, path);
217     return path;
218   }
219 
buildPath(ServiceDescriptor service)220   private String buildPath(ServiceDescriptor service) {
221     String path = descriptorToPath.get(service);
222     if (path != null) {
223       return path;
224     }
225     path =
226         String.format(
227             "%d.%d",
228             FileDescriptorProto.SERVICE_FIELD_NUMBER,
229             service.getFile().getServices().indexOf(service));
230     descriptorToPath.put(service, path);
231     return path;
232   }
233 
buildPath(MethodDescriptor method)234   private String buildPath(MethodDescriptor method) {
235     String path = descriptorToPath.get(method);
236     if (path != null) {
237       return path;
238     }
239     path =
240         String.format(
241             "%s.%d.%d",
242             buildPath(method.getService()),
243             ServiceDescriptorProto.METHOD_FIELD_NUMBER,
244             method.getService().getMethods().indexOf(method));
245     descriptorToPath.put(method, path);
246     return path;
247   }
248 
buildPath(EnumDescriptor enumType)249   private String buildPath(EnumDescriptor enumType) {
250     String path = descriptorToPath.get(enumType);
251     if (path != null) {
252       return path;
253     }
254     if (enumType.getContainingType() != null) {
255       path =
256           String.format(
257               "%s.%d.%d",
258               buildPath(enumType.getContainingType()),
259               DescriptorProto.ENUM_TYPE_FIELD_NUMBER,
260               enumType.getContainingType().getEnumTypes().indexOf(enumType));
261     } else {
262       path =
263           String.format(
264               "%d.%d",
265               FileDescriptorProto.ENUM_TYPE_FIELD_NUMBER,
266               enumType.getFile().getEnumTypes().indexOf(enumType));
267     }
268     descriptorToPath.put(enumType, path);
269     return path;
270   }
271 
buildPath(EnumValueDescriptor enumValue)272   private String buildPath(EnumValueDescriptor enumValue) {
273     String path = descriptorToPath.get(enumValue);
274     if (path != null) {
275       return path;
276     }
277     path =
278         String.format(
279             "%s.%d.%d",
280             buildPath(enumValue.getType()),
281             EnumDescriptorProto.VALUE_FIELD_NUMBER,
282             enumValue.getType().getValues().indexOf(enumValue));
283     descriptorToPath.put(enumValue, path);
284     return path;
285   }
286 
buildPath(OneofDescriptor oneof)287   private String buildPath(OneofDescriptor oneof) {
288     String path = descriptorToPath.get(oneof);
289     if (path != null) {
290       return path;
291     }
292     path =
293         String.format(
294             "%s.%d.%d",
295             buildPath(oneof.getContainingType()),
296             DescriptorProto.ONEOF_DECL_FIELD_NUMBER,
297             oneof.getContainingType().getOneofs().indexOf(oneof));
298 
299     descriptorToPath.put(oneof, path);
300     return path;
301   }
302 }
303