Skip to content

Commit f5cdbd0

Browse files
committed
Merge pull request #1555 from Ant00000ny
* pr/1555: Polish "Escape keywords in kotlin package declarations" Escape keywords in kotlin package declarations Closes gh-1555
2 parents e3dbdd4 + bce08cf commit f5cdbd0

File tree

10 files changed

+263
-33
lines changed

10 files changed

+263
-33
lines changed

initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/kotlin/KotlinProjectGenerationConfiguration.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,17 @@ public MainSourceCodeProjectContributor<KotlinTypeDeclaration, KotlinCompilation
7171
ObjectProvider<MainCompilationUnitCustomizer<?, ?>> mainCompilationUnitCustomizers,
7272
ObjectProvider<MainSourceCodeCustomizer<?, ?, ?>> mainSourceCodeCustomizers) {
7373
return new MainSourceCodeProjectContributor<>(this.description, KotlinSourceCode::new,
74-
new KotlinSourceCodeWriter(this.indentingWriterFactory), mainApplicationTypeCustomizers,
75-
mainCompilationUnitCustomizers, mainSourceCodeCustomizers);
74+
new KotlinSourceCodeWriter(this.description.getLanguage(), this.indentingWriterFactory),
75+
mainApplicationTypeCustomizers, mainCompilationUnitCustomizers, mainSourceCodeCustomizers);
7676
}
7777

7878
@Bean
7979
public TestSourceCodeProjectContributor<KotlinTypeDeclaration, KotlinCompilationUnit, KotlinSourceCode> testKotlinSourceCodeProjectContributor(
8080
ObjectProvider<TestApplicationTypeCustomizer<?>> testApplicationTypeCustomizers,
8181
ObjectProvider<TestSourceCodeCustomizer<?, ?, ?>> testSourceCodeCustomizers) {
8282
return new TestSourceCodeProjectContributor<>(this.description, KotlinSourceCode::new,
83-
new KotlinSourceCodeWriter(this.indentingWriterFactory), testApplicationTypeCustomizers,
84-
testSourceCodeCustomizers);
83+
new KotlinSourceCodeWriter(this.description.getLanguage(), this.indentingWriterFactory),
84+
testApplicationTypeCustomizers, testSourceCodeCustomizers);
8585
}
8686

8787
@Bean

initializr-generator/src/main/java/io/spring/initializr/generator/language/Language.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* A language in which a generated project can be written.
2525
*
2626
* @author Andy Wilkinson
27+
* @author Moritz Halbritter
2728
*/
2829
public interface Language {
2930

@@ -50,6 +51,19 @@ public interface Language {
5051
*/
5152
String sourceFileExtension();
5253

54+
/**
55+
* Whether the language supports escaping keywords in package declarations.
56+
* @return whether the language supports escaping keywords in package declarations.
57+
*/
58+
boolean supportsEscapingKeywordsInPackage();
59+
60+
/**
61+
* Whether the given {@code input} is a keyword.
62+
* @param input the input
63+
* @return whether the input is a keyword
64+
*/
65+
boolean isKeyword(String input);
66+
5367
static Language forId(String id, String jvmVersion) {
5468
return SpringFactoriesLoader.loadFactories(LanguageFactory.class, LanguageFactory.class.getClassLoader())
5569
.stream()

initializr-generator/src/main/java/io/spring/initializr/generator/language/groovy/GroovyLanguage.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,26 @@
1616

1717
package io.spring.initializr.generator.language.groovy;
1818

19+
import java.util.Set;
20+
1921
import io.spring.initializr.generator.language.AbstractLanguage;
2022
import io.spring.initializr.generator.language.Language;
2123

2224
/**
2325
* Groovy {@link Language}.
2426
*
2527
* @author Stephane Nicoll
28+
* @author Moritz Halbritter
2629
*/
2730
public final class GroovyLanguage extends AbstractLanguage {
2831

32+
// See https://docs.groovy-lang.org/latest/html/documentation/#_keywords
33+
private static final Set<String> KEYWORDS = Set.of("abstract", "assert", "break", "case", "catch", "class", "const",
34+
"continue", "def", "default", "do", "else", "enum", "extends", "final", "finally", "for", "goto", "if",
35+
"implements", "import", "instanceof", "interface", "native", "new", "null", "non-sealed", "package",
36+
"public", "protected", "private", "return", "static", "strictfp", "super", "switch", "synchronized", "this",
37+
"threadsafe", "throw", "throws", "transient", "try", "while");
38+
2939
/**
3040
* Groovy {@link Language} identifier.
3141
*/
@@ -39,4 +49,14 @@ public GroovyLanguage(String jvmVersion) {
3949
super(ID, jvmVersion, "groovy");
4050
}
4151

52+
@Override
53+
public boolean supportsEscapingKeywordsInPackage() {
54+
return false;
55+
}
56+
57+
@Override
58+
public boolean isKeyword(String input) {
59+
return KEYWORDS.contains(input);
60+
}
61+
4262
}

initializr-generator/src/main/java/io/spring/initializr/generator/language/java/JavaLanguage.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package io.spring.initializr.generator.language.java;
1818

19+
import javax.lang.model.SourceVersion;
20+
1921
import io.spring.initializr.generator.language.AbstractLanguage;
2022
import io.spring.initializr.generator.language.Language;
2123

@@ -40,4 +42,14 @@ public JavaLanguage(String jvmVersion) {
4042
super(ID, jvmVersion, "java");
4143
}
4244

45+
@Override
46+
public boolean supportsEscapingKeywordsInPackage() {
47+
return false;
48+
}
49+
50+
@Override
51+
public boolean isKeyword(String input) {
52+
return SourceVersion.isKeyword(input);
53+
}
54+
4355
}

initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinLanguage.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,26 @@
1616

1717
package io.spring.initializr.generator.language.kotlin;
1818

19+
import java.util.Set;
20+
1921
import io.spring.initializr.generator.language.AbstractLanguage;
2022
import io.spring.initializr.generator.language.Language;
2123

2224
/**
2325
* Kotlin {@link Language}.
2426
*
2527
* @author Stephane Nicoll
28+
* @author Moritz Halbritter
2629
*/
2730
public final class KotlinLanguage extends AbstractLanguage {
2831

32+
// Taken from https://kotlinlang.org/docs/keyword-reference.html#hard-keywords
33+
// except keywords contains `!` or `?` because they should be handled as invalid
34+
// package names already
35+
private static final Set<String> KEYWORDS = Set.of("package", "as", "typealias", "class", "this", "super", "val",
36+
"var", "fun", "for", "null", "true", "false", "is", "in", "throw", "return", "break", "continue", "object",
37+
"if", "try", "else", "while", "do", "when", "interface", "typeof");
38+
2939
/**
3040
* Kotlin {@link Language} identifier.
3141
*/
@@ -39,4 +49,14 @@ public KotlinLanguage(String jvmVersion) {
3949
super(ID, jvmVersion, "kt");
4050
}
4151

52+
@Override
53+
public boolean supportsEscapingKeywordsInPackage() {
54+
return true;
55+
}
56+
57+
@Override
58+
public boolean isKeyword(String input) {
59+
return KEYWORDS.contains(input);
60+
}
61+
4262
}

initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import io.spring.initializr.generator.language.CodeBlock;
4141
import io.spring.initializr.generator.language.CodeBlock.FormattingOptions;
4242
import io.spring.initializr.generator.language.CompilationUnit;
43+
import io.spring.initializr.generator.language.Language;
4344
import io.spring.initializr.generator.language.Parameter;
4445
import io.spring.initializr.generator.language.SourceCode;
4546
import io.spring.initializr.generator.language.SourceCodeWriter;
@@ -55,9 +56,12 @@ public class KotlinSourceCodeWriter implements SourceCodeWriter<KotlinSourceCode
5556

5657
private static final FormattingOptions FORMATTING_OPTIONS = new KotlinFormattingOptions();
5758

59+
private final Language language;
60+
5861
private final IndentingWriterFactory indentingWriterFactory;
5962

60-
public KotlinSourceCodeWriter(IndentingWriterFactory indentingWriterFactory) {
63+
public KotlinSourceCodeWriter(Language language, IndentingWriterFactory indentingWriterFactory) {
64+
this.language = language;
6165
this.indentingWriterFactory = indentingWriterFactory;
6266
}
6367

@@ -73,7 +77,7 @@ private void writeTo(SourceStructure structure, KotlinCompilationUnit compilatio
7377
Files.createDirectories(output.getParent());
7478
try (IndentingWriter writer = this.indentingWriterFactory.createIndentingWriter("kotlin",
7579
Files.newBufferedWriter(output))) {
76-
writer.println("package " + compilationUnit.getPackageName());
80+
writer.println("package " + escapeKotlinKeywords(compilationUnit.getPackageName()));
7781
writer.println();
7882
Set<String> imports = determineImports(compilationUnit);
7983
if (!imports.isEmpty()) {
@@ -127,6 +131,12 @@ private void writeTo(SourceStructure structure, KotlinCompilationUnit compilatio
127131
}
128132
}
129133

134+
private String escapeKotlinKeywords(String packageName) {
135+
return Arrays.stream(packageName.split("\\."))
136+
.map((segment) -> this.language.isKeyword(segment) ? "`" + segment + "`" : segment)
137+
.collect(Collectors.joining("."));
138+
}
139+
130140
private void writeProperty(IndentingWriter writer, KotlinPropertyDeclaration propertyDeclaration) {
131141
writer.println();
132142
writeModifiers(writer, propertyDeclaration.getModifiers());

initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class KotlinSourceCodeWriterTests {
5555
@TempDir
5656
Path directory;
5757

58-
private final KotlinSourceCodeWriter writer = new KotlinSourceCodeWriter(
58+
private final KotlinSourceCodeWriter writer = new KotlinSourceCodeWriter(new KotlinLanguage(),
5959
IndentingWriterFactory.withDefaultSettings());
6060

6161
@Test
@@ -361,6 +361,54 @@ void functionWithParameterAnnotation() throws IOException {
361361
" fun something(@Service service: MyService) {", " }", "", "}");
362362
}
363363

364+
@Test
365+
void reservedKeywordsStartPackageName() throws IOException {
366+
KotlinSourceCode sourceCode = new KotlinSourceCode();
367+
sourceCode.createCompilationUnit("fun.example.demo", "Test");
368+
List<String> lines = writeSingleType(sourceCode, "fun/example/demo/Test.kt");
369+
assertThat(lines).containsExactly("package `fun`.example.demo");
370+
}
371+
372+
@Test
373+
void reservedKeywordsMiddlePackageName() throws IOException {
374+
KotlinSourceCode sourceCode = new KotlinSourceCode();
375+
sourceCode.createCompilationUnit("com.false.demo", "Test");
376+
List<String> lines = writeSingleType(sourceCode, "com/false/demo/Test.kt");
377+
assertThat(lines).containsExactly("package com.`false`.demo");
378+
}
379+
380+
@Test
381+
void reservedKeywordsEndPackageName() throws IOException {
382+
KotlinSourceCode sourceCode = new KotlinSourceCode();
383+
sourceCode.createCompilationUnit("com.example.in", "Test");
384+
List<String> lines = writeSingleType(sourceCode, "com/example/in/Test.kt");
385+
assertThat(lines).containsExactly("package com.example.`in`");
386+
}
387+
388+
@Test
389+
void reservedJavaKeywordsStartPackageName() throws IOException {
390+
KotlinSourceCode sourceCode = new KotlinSourceCode();
391+
sourceCode.createCompilationUnit("package.fun.example.demo", "Test");
392+
List<String> lines = writeSingleType(sourceCode, "package/fun/example/demo/Test.kt");
393+
assertThat(lines).containsExactly("package `package`.`fun`.example.demo");
394+
}
395+
396+
@Test
397+
void reservedJavaKeywordsMiddlePackageName() throws IOException {
398+
KotlinSourceCode sourceCode = new KotlinSourceCode();
399+
sourceCode.createCompilationUnit("com.package.demo", "Test");
400+
List<String> lines = writeSingleType(sourceCode, "com/package/demo/Test.kt");
401+
assertThat(lines).containsExactly("package com.`package`.demo");
402+
}
403+
404+
@Test
405+
void reservedJavaKeywordsEndPackageName() throws IOException {
406+
KotlinSourceCode sourceCode = new KotlinSourceCode();
407+
sourceCode.createCompilationUnit("com.example.package", "Test");
408+
List<String> lines = writeSingleType(sourceCode, "com/example/package/Test.kt");
409+
assertThat(lines).containsExactly("package com.example.`package`");
410+
}
411+
364412
private List<String> writeSingleType(KotlinSourceCode sourceCode, String location) throws IOException {
365413
Path source = writeSourceCode(sourceCode).resolve(location);
366414
try (InputStream stream = Files.newInputStream(source)) {

initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@
2525
import java.util.List;
2626
import java.util.Map;
2727

28-
import javax.lang.model.SourceVersion;
29-
3028
import com.fasterxml.jackson.annotation.JsonIgnore;
29+
import io.spring.initializr.generator.language.Language;
3130
import io.spring.initializr.generator.version.InvalidVersionException;
3231
import io.spring.initializr.generator.version.Version;
3332
import io.spring.initializr.generator.version.Version.Format;
@@ -103,11 +102,12 @@ public String generateApplicationName(String name) {
103102
* The package name cannot be cleaned if the specified {@code packageName} is
104103
* {@code null} or if it contains an invalid character for a class identifier.
105104
* @param packageName the package name
105+
* @param language the project language
106106
* @param defaultPackageName the default package name
107107
* @return the cleaned package name
108108
* @see Env#getInvalidPackageNames()
109109
*/
110-
public String cleanPackageName(String packageName, String defaultPackageName) {
110+
public String cleanPackageName(String packageName, Language language, String defaultPackageName) {
111111
if (!StringUtils.hasText(packageName)) {
112112
return defaultPackageName;
113113
}
@@ -118,12 +118,16 @@ public String cleanPackageName(String packageName, String defaultPackageName) {
118118
if (hasInvalidChar(candidate.replace(".", "")) || this.env.invalidPackageNames.contains(candidate)) {
119119
return defaultPackageName;
120120
}
121-
if (hasReservedKeyword(candidate)) {
122-
return defaultPackageName;
123-
}
124-
else {
125-
return candidate;
121+
if (!supportsEscapingKeywordsInPackage(language)) {
122+
if (hasReservedKeyword(language, candidate)) {
123+
return defaultPackageName;
124+
}
126125
}
126+
return candidate;
127+
}
128+
129+
private boolean supportsEscapingKeywordsInPackage(Language language) {
130+
return (language != null) ? language.supportsEscapingKeywordsInPackage() : false;
127131
}
128132

129133
static String cleanPackageName(String packageName) {
@@ -165,8 +169,11 @@ private static boolean hasInvalidChar(String text) {
165169
return false;
166170
}
167171

168-
private static boolean hasReservedKeyword(final String packageName) {
169-
return Arrays.stream(packageName.split("\\.")).anyMatch(SourceVersion::isKeyword);
172+
private static boolean hasReservedKeyword(Language language, String packageName) {
173+
if (language == null) {
174+
return false;
175+
}
176+
return Arrays.stream(packageName.split("\\.")).anyMatch(language::isKeyword);
170177
}
171178

172179
/**

0 commit comments

Comments
 (0)