Skip to content

Commit 403dea5

Browse files
mkruskal-googlecopybara-github
authored andcommitted
Fix a bug calculating the file name in the absense of directories.
PiperOrigin-RevId: 814352590
1 parent e84d171 commit 403dea5

17 files changed

+881
-1
lines changed

java/core/BUILD.bazel

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ proto_lang_toolchain(
347347

348348
protobuf_test_proto_library(
349349
name = "java_test_protos",
350-
srcs = glob(["src/test/proto/**/*.proto"]),
350+
srcs = glob([
351+
"src/test/proto/**/*.proto",
352+
"src/test/proto/**/*.protodevel",
353+
]),
351354
option_deps = [
352355
"//:java_features_proto",
353356
],
@@ -551,6 +554,7 @@ LITE_TEST_EXCLUSIONS = [
551554
"src/test/java/com/google/protobuf/FieldPresenceTest.java",
552555
"src/test/java/com/google/protobuf/ForceFieldBuildersPreRun.java",
553556
"src/test/java/com/google/protobuf/GeneratedMessageTest.java",
557+
"src/test/java/com/google/protobuf/GeneratorNamesTest.java",
554558
"src/test/java/com/google/protobuf/ImportOptionTest.java",
555559
"src/test/java/com/google/protobuf/LazilyParsedMessageSetTest.java",
556560
"src/test/java/com/google/protobuf/LazyFieldTest.java",
@@ -638,6 +642,7 @@ junit_tests(
638642
"src/test/java/com/google/protobuf/CodedInputStreamTest.java",
639643
"src/test/java/com/google/protobuf/ProtobufToStringOutputTest.java",
640644
"src/test/java/com/google/protobuf/FieldPresenceTest.java",
645+
"src/test/java/com/google/protobuf/GeneratorNamesTest.java",
641646
"src/test/java/com/google/protobuf/UnredactedDebugFormatForTestTest.java",
642647
"src/test/java/com/google/protobuf/LargeEnumTest.java",
643648
"src/test/java/com/google/protobuf/LargeEnumLiteTest.java",
@@ -692,6 +697,7 @@ junit_tests(
692697
"src/test/java/com/google/protobuf/CodedInputStreamTest.java",
693698
"src/test/java/com/google/protobuf/ProtobufToStringOutputTest.java",
694699
"src/test/java/com/google/protobuf/FieldPresenceTest.java",
700+
"src/test/java/com/google/protobuf/GeneratorNamesTest.java",
695701
"src/test/java/com/google/protobuf/LargeEnumTest.java",
696702
"src/test/java/com/google/protobuf/LargeEnumLiteTest.java",
697703
"src/test/java/com/google/protobuf/ImportOptionTest.java",
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
package com.google.protobuf;
2+
3+
import com.google.protobuf.DescriptorProtos.DescriptorProto;
4+
import com.google.protobuf.DescriptorProtos.Edition;
5+
import com.google.protobuf.DescriptorProtos.EnumDescriptorProto;
6+
import com.google.protobuf.DescriptorProtos.FeatureSet;
7+
import com.google.protobuf.DescriptorProtos.FileDescriptorProtoOrBuilder;
8+
import com.google.protobuf.DescriptorProtos.FileOptions;
9+
import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto;
10+
import com.google.protobuf.Descriptors.Descriptor;
11+
import com.google.protobuf.Descriptors.EnumDescriptor;
12+
import com.google.protobuf.Descriptors.FileDescriptor;
13+
import com.google.protobuf.Descriptors.ServiceDescriptor;
14+
import com.google.protobuf.GeneratedMessage.GeneratedExtension;
15+
import com.google.protobuf.JavaFeaturesProto.JavaFeatures;
16+
17+
/** Class containing helper methods for predicting names of generated java classes. */
18+
public final class GeneratorNames {
19+
20+
private GeneratorNames() {}
21+
22+
/** Returns the generated package for the given file descriptor proto. */
23+
public static String getFileJavaPackage(FileDescriptorProtoOrBuilder file) {
24+
return getProto2ApiDefaultJavaPackage(file.getOptions(), file.getPackage());
25+
}
26+
27+
/** Returns the generated package for the given file descriptor. */
28+
public static String getFileJavaPackage(FileDescriptor file) {
29+
return getProto2ApiDefaultJavaPackage(file.getOptions(), file.getPackage());
30+
}
31+
32+
/** Returns the default java package for the given file. */
33+
static String getDefaultJavaPackage(FileOptions fileOptions, String filePackage) {
34+
// Replicates the logic from DefaultJavaPackage in java/names_internal.h.
35+
if (fileOptions.hasJavaPackage()) {
36+
return fileOptions.getJavaPackage();
37+
} else {
38+
return filePackage;
39+
}
40+
}
41+
42+
/** Joins two package segments into a single package name with a dot separator. */
43+
static String joinPackage(String a, String b) {
44+
if (a.isEmpty()) {
45+
return b;
46+
} else if (b.isEmpty()) {
47+
return a;
48+
} else {
49+
return a + '.' + b;
50+
}
51+
}
52+
53+
/**
54+
* Returns the package name to use for a file that is being compiled as proto2-API. If the file is
55+
* declared as proto1-API, this may involve using the alternate package name.
56+
*/
57+
static String getProto2ApiDefaultJavaPackage(FileOptions fileOptions, String filePackage) {
58+
// Replicates the logic from Proto2DefaultJavaPackage in java/names_internal.h.
59+
return getDefaultJavaPackage(fileOptions, filePackage);
60+
}
61+
62+
/**
63+
* Returns the generated unqualified outer file class name for the given file descriptor proto.
64+
*/
65+
public static String getFileClassName(FileDescriptorProtoOrBuilder file) {
66+
return getFileClassNameImpl(file, getResolvedFileFeatures(JavaFeaturesProto.java_, file));
67+
}
68+
69+
/** Returns the generated unqualified outer file class name for the given file descriptor. */
70+
public static String getFileClassName(FileDescriptor file) {
71+
return getFileClassNameImpl(
72+
file.toProto(), file.getFeatures().getExtension(JavaFeaturesProto.java_));
73+
}
74+
75+
private static String getFileClassNameImpl(
76+
FileDescriptorProtoOrBuilder file, JavaFeatures resolvedFeatures) {
77+
// Replicates the logic of ClassNameResolver::GetFileImmutableClassName.
78+
if (file.getOptions().hasJavaOuterClassname()) {
79+
return file.getOptions().getJavaOuterClassname();
80+
}
81+
82+
String className =
83+
getDefaultFileClassName(file, resolvedFeatures.getUseOldOuterClassnameDefault());
84+
if (resolvedFeatures.getUseOldOuterClassnameDefault()
85+
&& hasConflictingClassName(file, className)) {
86+
return className + "OuterClass";
87+
}
88+
return className;
89+
}
90+
91+
/**
92+
* Returns the resolved features for the given descriptor proto.
93+
*
94+
* <p>This method isn't actually naming-specific, but lives here for now because it's the only
95+
* place it's needed. Once we have a better home for dealing with features in code generators,
96+
* this should move.
97+
*/
98+
@SuppressWarnings("unchecked")
99+
static <T extends Message> T getResolvedFileFeatures(
100+
GeneratedExtension<FeatureSet, T> ext, FileDescriptorProtoOrBuilder file) {
101+
Edition edition;
102+
if (file.getSyntax().equals("proto3")) {
103+
edition = Edition.EDITION_PROTO3;
104+
} else if (!file.hasEdition()) {
105+
edition = Edition.EDITION_PROTO2;
106+
} else {
107+
edition = file.getEdition();
108+
}
109+
FeatureSet features = file.getOptions().getFeatures();
110+
if (features.getUnknownFields().hasField(ext.getNumber())) {
111+
// The incoming descriptor proto may not have been parsed with the right extension registry,
112+
// in which case we need to parse it again with the right extensions.
113+
ExtensionRegistry registry = ExtensionRegistry.newInstance();
114+
registry.add(ext);
115+
try {
116+
features =
117+
FeatureSet.newBuilder()
118+
.mergeFrom(features.getUnknownFields().toByteString(), registry)
119+
.build();
120+
} catch (InvalidProtocolBufferException e) {
121+
// This should never happen, since the unknown fields have already been parsed and features
122+
// are always integral types.
123+
throw new IllegalArgumentException("Failed to parse features", e);
124+
}
125+
}
126+
return (T)
127+
Descriptors.getEditionDefaults(edition).getExtension(ext).toBuilder()
128+
.mergeFrom(features.getExtension(ext))
129+
.build();
130+
}
131+
132+
/**
133+
* Returns the default unqualified file class name for the given file.
134+
*
135+
* @param file the file descriptor proto
136+
* @param useOldOuterClassnameDefault whether to use the old default for the outer classname
137+
*/
138+
static String getDefaultFileClassName(
139+
FileDescriptorProtoOrBuilder file, boolean useOldOuterClassnameDefault) {
140+
// Replicates the logic of ClassNameResolver::GetFileDefaultImmutableClassName.
141+
String name = file.getName();
142+
name = name.substring(name.lastIndexOf('/') + 1);
143+
name = underscoresToCamelCase(stripProto(name));
144+
return useOldOuterClassnameDefault ? name : name + "Proto";
145+
}
146+
147+
private static String stripProto(String filename) {
148+
// Replicates the logic of ClassNameResolver::StripProto.
149+
if (filename.endsWith(".protodevel")) {
150+
return filename.substring(0, filename.length() - ".protodevel".length());
151+
}
152+
if (filename.endsWith(".proto")) {
153+
return filename.substring(0, filename.length() - ".proto".length());
154+
}
155+
156+
return filename;
157+
}
158+
159+
/** Checks whether any generated classes conflict with the given name. */
160+
private static boolean hasConflictingClassName(FileDescriptorProtoOrBuilder file, String name) {
161+
for (EnumDescriptorProto enumDesc : file.getEnumTypeList()) {
162+
if (name.equals(enumDesc.getName())) {
163+
return true;
164+
}
165+
}
166+
for (ServiceDescriptorProto serviceDesc : file.getServiceList()) {
167+
if (name.equals(serviceDesc.getName())) {
168+
return true;
169+
}
170+
}
171+
for (DescriptorProto messageDesc : file.getMessageTypeList()) {
172+
if (hasConflictingClassName(messageDesc, name)) {
173+
return true;
174+
}
175+
}
176+
return false;
177+
}
178+
179+
/** Used by the other overload, descends recursively into messages. */
180+
private static boolean hasConflictingClassName(DescriptorProto messageDesc, String name) {
181+
if (name.equals(messageDesc.getName())) {
182+
return true;
183+
}
184+
for (EnumDescriptorProto enumDesc : messageDesc.getEnumTypeList()) {
185+
if (name.equals(enumDesc.getName())) {
186+
return true;
187+
}
188+
}
189+
for (DescriptorProto nestedMessageDesc : messageDesc.getNestedTypeList()) {
190+
if (hasConflictingClassName(nestedMessageDesc, name)) {
191+
return true;
192+
}
193+
}
194+
return false;
195+
}
196+
197+
/**
198+
* Converts a name to camel-case.
199+
*
200+
* @param input the input string to convert
201+
* @param capitalizeNextLetter whether to capitalize the first letter
202+
*/
203+
static String underscoresToCamelCase(String input, boolean capitalizeNextLetter) {
204+
// Replicates the logic of ClassNameResolver::UnderscoresToCamelCase.
205+
StringBuilder result = new StringBuilder();
206+
for (int i = 0; i < input.length(); i++) {
207+
char ch = input.charAt(i);
208+
if ('a' <= ch && ch <= 'z') {
209+
if (capitalizeNextLetter) {
210+
result.append((char) (ch + ('A' - 'a')));
211+
} else {
212+
result.append(ch);
213+
}
214+
capitalizeNextLetter = false;
215+
} else if ('A' <= ch && ch <= 'Z') {
216+
if (i == 0 && !capitalizeNextLetter) {
217+
// Force first letter to lower-case unless explicitly told to
218+
// capitalize it.
219+
result.append((char) (ch + ('a' - 'A')));
220+
} else {
221+
// Capital letters after the first are left as-is.
222+
result.append(ch);
223+
}
224+
capitalizeNextLetter = false;
225+
} else if ('0' <= ch && ch <= '9') {
226+
result.append(ch);
227+
capitalizeNextLetter = true;
228+
} else {
229+
capitalizeNextLetter = true;
230+
}
231+
}
232+
return result.toString();
233+
}
234+
235+
static String underscoresToCamelCase(String input) {
236+
return underscoresToCamelCase(input, /* capitalizeNextLetter= */ true);
237+
}
238+
239+
/** Returns the fully qualified Java class name for the given message descriptor. */
240+
public static String getClassName(Descriptor message) {
241+
// Replicates the logic for ClassName from immutable/names.h
242+
return getClassFullName(
243+
getClassNameWithoutPackage(message), message.getFile(), !getNestInFileClass(message));
244+
}
245+
246+
/** Returns the fully qualified Java class name for the given enum descriptor. */
247+
public static String getClassName(EnumDescriptor enm) {
248+
// Replicates the logic for ClassName from immutable/names.h
249+
return getClassFullName(
250+
getClassNameWithoutPackage(enm), enm.getFile(), !getNestInFileClass(enm));
251+
}
252+
253+
/** Returns the fully qualified Java class name for the given service descriptor. */
254+
static String getClassName(ServiceDescriptor service) {
255+
// Replicates the logic for ClassName from immutable/names.h
256+
String suffix = "";
257+
boolean isOwnFile = !getNestInFileClass(service);
258+
return getClassFullName(getClassNameWithoutPackage(service), service.getFile(), isOwnFile)
259+
+ suffix;
260+
}
261+
262+
private static String getClassFullName(
263+
String nameWithoutPackage, FileDescriptor file, boolean isOwnFile) {
264+
// Replicates the logic for ClassNameResolver::GetJavaClassFullName from immutable/names.cc
265+
StringBuilder result = new StringBuilder();
266+
if (isOwnFile) {
267+
result.append(getFileJavaPackage(file.toProto()));
268+
if (result.length() > 0) {
269+
result.append(".");
270+
}
271+
} else {
272+
result.append(joinPackage(getFileJavaPackage(file.toProto()), getFileClassName(file)));
273+
if (result.length() > 0) {
274+
result.append("$");
275+
}
276+
}
277+
result.append(nameWithoutPackage.replace('.', '$'));
278+
return result.toString();
279+
}
280+
281+
/** Returns the nest_in_file_class behavior for a given set of features in a specific file. */
282+
// Switch expressions were released in Java 14, and we support Java 8.
283+
@SuppressWarnings("StatementSwitchToExpressionSwitch")
284+
private static boolean getNestInFileClass(FileDescriptor file, JavaFeatures resolvedFeatures) {
285+
switch (resolvedFeatures.getNestInFileClass()) {
286+
case YES:
287+
return true;
288+
case NO:
289+
return false;
290+
case LEGACY:
291+
return !file.getOptions().getJavaMultipleFiles();
292+
default:
293+
throw new IllegalArgumentException("Java features are not resolved");
294+
}
295+
}
296+
297+
private static boolean getNestInFileClass(Descriptor descriptor) {
298+
return getNestInFileClass(
299+
descriptor.getFile(), descriptor.getFeatures().getExtension(JavaFeaturesProto.java_));
300+
}
301+
302+
private static boolean getNestInFileClass(EnumDescriptor descriptor) {
303+
return getNestInFileClass(
304+
descriptor.getFile(), descriptor.getFeatures().getExtension(JavaFeaturesProto.java_));
305+
}
306+
307+
private static boolean getNestInFileClass(ServiceDescriptor descriptor) {
308+
return getNestInFileClass(
309+
descriptor.getFile(), descriptor.getFeatures().getExtension(JavaFeaturesProto.java_));
310+
}
311+
312+
/** Returns the name of the given descriptor without the package name prefix. */
313+
static String stripPackageName(String fullName, FileDescriptor file) {
314+
if (file.getPackage().isEmpty()) {
315+
return fullName;
316+
} else {
317+
return fullName.substring(file.getPackage().length() + 1);
318+
}
319+
}
320+
321+
/** Returns the name of the given message descriptor without the package name prefix. */
322+
static String getClassNameWithoutPackage(Descriptor message) {
323+
return stripPackageName(message.getFullName(), message.getFile());
324+
}
325+
326+
/** Returns the name of the given enum descriptor without the package name prefix. */
327+
static String getClassNameWithoutPackage(EnumDescriptor enm) {
328+
Descriptor containingType = enm.getContainingType();
329+
if (containingType == null) {
330+
return enm.getName();
331+
}
332+
return joinPackage(getClassNameWithoutPackage(containingType), enm.getName());
333+
}
334+
335+
/** Returns the name of the given service descriptor without the package name prefix. */
336+
static String getClassNameWithoutPackage(ServiceDescriptor service) {
337+
return stripPackageName(service.getFullName(), service.getFile());
338+
}
339+
}

0 commit comments

Comments
 (0)