Skip to content

Commit

Permalink
Escape keywords in kotlin package declarations
Browse files Browse the repository at this point in the history
  • Loading branch information
Ant00000ny authored and mhalbritter committed Aug 7, 2024
1 parent e3dbdd4 commit 776e662
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ public class KotlinSourceCodeWriter implements SourceCodeWriter<KotlinSourceCode

private static final FormattingOptions FORMATTING_OPTIONS = new KotlinFormattingOptions();

// Taken from https://kotlinlang.org/docs/keyword-reference.html#hard-keywords
// except keywords contains `!` or `?` because they should be handled as invalid
// package names already
private static final Set<String> KOTLIN_HARD_KEYWORDS = Set.of("package", "as", "typealias", "class", "this",
"super", "val", "var", "fun", "for", "null", "true", "false", "is", "in", "throw", "return", "break",
"continue", "object", "if", "try", "else", "while", "do", "when", "interface", "typeof");

private final IndentingWriterFactory indentingWriterFactory;

public KotlinSourceCodeWriter(IndentingWriterFactory indentingWriterFactory) {
Expand All @@ -68,12 +75,18 @@ public void writeTo(SourceStructure structure, KotlinSourceCode sourceCode) thro
}
}

private static String escapeKotlinKeywords(String packageName) {
return Arrays.stream(packageName.split("\\."))
.map((segment) -> KOTLIN_HARD_KEYWORDS.contains(segment) ? "`" + segment + "`" : segment)
.collect(Collectors.joining("."));
}

private void writeTo(SourceStructure structure, KotlinCompilationUnit compilationUnit) throws IOException {
Path output = structure.createSourceFile(compilationUnit.getPackageName(), compilationUnit.getName());
Files.createDirectories(output.getParent());
try (IndentingWriter writer = this.indentingWriterFactory.createIndentingWriter("kotlin",
Files.newBufferedWriter(output))) {
writer.println("package " + compilationUnit.getPackageName());
writer.println("package " + escapeKotlinKeywords(compilationUnit.getPackageName()));
writer.println();
Set<String> imports = determineImports(compilationUnit);
if (!imports.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,54 @@ void functionWithParameterAnnotation() throws IOException {
" fun something(@Service service: MyService) {", " }", "", "}");
}

@Test
void reservedKeywordsStartPackageName() throws IOException {
KotlinSourceCode sourceCode = new KotlinSourceCode();
sourceCode.createCompilationUnit("fun.example.demo", "Test");
List<String> lines = writeSingleType(sourceCode, "fun/example/demo/Test.kt");
assertThat(lines).containsExactly("package `fun`.example.demo");
}

@Test
void reservedKeywordsMiddlePackageName() throws IOException {
KotlinSourceCode sourceCode = new KotlinSourceCode();
sourceCode.createCompilationUnit("com.false.demo", "Test");
List<String> lines = writeSingleType(sourceCode, "com/false/demo/Test.kt");
assertThat(lines).containsExactly("package com.`false`.demo");
}

@Test
void reservedKeywordsEndPackageName() throws IOException {
KotlinSourceCode sourceCode = new KotlinSourceCode();
sourceCode.createCompilationUnit("com.example.in", "Test");
List<String> lines = writeSingleType(sourceCode, "com/example/in/Test.kt");
assertThat(lines).containsExactly("package com.example.`in`");
}

@Test
void reservedJavaKeywordsStartPackageName() throws IOException {
KotlinSourceCode sourceCode = new KotlinSourceCode();
sourceCode.createCompilationUnit("package.fun.example.demo", "Test");
List<String> lines = writeSingleType(sourceCode, "package/fun/example/demo/Test.kt");
assertThat(lines).containsExactly("package `package`.`fun`.example.demo");
}

@Test
void reservedJavaKeywordsMiddlePackageName() throws IOException {
KotlinSourceCode sourceCode = new KotlinSourceCode();
sourceCode.createCompilationUnit("com.package.demo", "Test");
List<String> lines = writeSingleType(sourceCode, "com/package/demo/Test.kt");
assertThat(lines).containsExactly("package com.`package`.demo");
}

@Test
void reservedJavaKeywordsEndPackageName() throws IOException {
KotlinSourceCode sourceCode = new KotlinSourceCode();
sourceCode.createCompilationUnit("com.example.package", "Test");
List<String> lines = writeSingleType(sourceCode, "com/example/package/Test.kt");
assertThat(lines).containsExactly("package com.example.`package`");
}

private List<String> writeSingleType(KotlinSourceCode sourceCode, String location) throws IOException {
Path source = writeSourceCode(sourceCode).resolve(location);
try (InputStream stream = Files.newInputStream(source)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,12 @@ public String generateApplicationName(String name) {
* The package name cannot be cleaned if the specified {@code packageName} is
* {@code null} or if it contains an invalid character for a class identifier.
* @param packageName the package name
* @param isKotlin if the package name clean is for kotlin project
* @param defaultPackageName the default package name
* @return the cleaned package name
* @see Env#getInvalidPackageNames()
*/
public String cleanPackageName(String packageName, String defaultPackageName) {
public String cleanPackageName(String packageName, boolean isKotlin, String defaultPackageName) {
if (!StringUtils.hasText(packageName)) {
return defaultPackageName;
}
Expand All @@ -118,7 +119,9 @@ public String cleanPackageName(String packageName, String defaultPackageName) {
if (hasInvalidChar(candidate.replace(".", "")) || this.env.invalidPackageNames.contains(candidate)) {
return defaultPackageName;
}
if (hasReservedKeyword(candidate)) {

// No check for Kotlin as its reserved keywords will be escaped later
if (!isKotlin && hasReservedKeyword(candidate)) {
return defaultPackageName;
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,77 +119,166 @@ void generateApplicationNameAnotherInvalidApplicationName() {

@Test
void generatePackageNameSimple() {
assertThat(this.properties.cleanPackageName("com.foo", "com.example")).isEqualTo("com.foo");
assertThat(this.properties.cleanPackageName("com.foo", false, "com.example")).isEqualTo("com.foo");
}

@Test
void generatePackageNameSimpleUnderscore() {
assertThat(this.properties.cleanPackageName("com.my_foo", "com.example")).isEqualTo("com.my_foo");
assertThat(this.properties.cleanPackageName("com.my_foo", false, "com.example")).isEqualTo("com.my_foo");
}

@Test
void generatePackageNameSimpleColon() {
assertThat(this.properties.cleanPackageName("com:foo", "com.example")).isEqualTo("com.foo");
assertThat(this.properties.cleanPackageName("com:foo", false, "com.example")).isEqualTo("com.foo");
}

@Test
void generatePackageNameMultipleDashes() {
assertThat(this.properties.cleanPackageName("com.foo--bar", "com.example")).isEqualTo("com.foo__bar");
assertThat(this.properties.cleanPackageName("com.foo--bar", false, "com.example")).isEqualTo("com.foo__bar");
}

@Test
void generatePackageNameMultipleSpaces() {
assertThat(this.properties.cleanPackageName(" com foo ", "com.example")).isEqualTo("com.foo");
assertThat(this.properties.cleanPackageName(" com foo ", false, "com.example")).isEqualTo("com.foo");
}

@Test
void generatePackageNameNull() {
assertThat(this.properties.cleanPackageName(null, "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName(null, false, "com.example")).isEqualTo("com.example");
}

@Test
void generatePackageNameDot() {
assertThat(this.properties.cleanPackageName(".", "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName(".", false, "com.example")).isEqualTo("com.example");
}

@Test
void generatePackageNameWhitespaces() {
assertThat(this.properties.cleanPackageName(" ", "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName(" ", false, "com.example")).isEqualTo("com.example");
}

@Test
void generatePackageNameInvalidStartCharacter() {
assertThat(this.properties.cleanPackageName("0com.foo", "com.example")).isEqualTo("_com.foo");
assertThat(this.properties.cleanPackageName("0com.foo", false, "com.example")).isEqualTo("_com.foo");
}

@Test
void generatePackageNameVersion() {
assertThat(this.properties.cleanPackageName("com.foo.test-1.4.5", "com.example")).isEqualTo("com.foo.test_145");
assertThat(this.properties.cleanPackageName("com.foo.test-1.4.5", false, "com.example"))
.isEqualTo("com.foo.test_145");
}

@Test
void generatePackageNameInvalidPackageName() {
assertThat(this.properties.cleanPackageName("org.springframework", "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName("org.springframework", false, "com.example"))
.isEqualTo("com.example");
}

@Test
void generatePackageNameReservedKeywordsMiddleOfPackageName() {
assertThat(this.properties.cleanPackageName("com.return.foo", "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName("com.return.foo", false, "com.example")).isEqualTo("com.example");
}

@Test
void generatePackageNameReservedKeywordsStartOfPackageName() {
assertThat(this.properties.cleanPackageName("false.com.foo", "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName("false.com.foo", false, "com.example")).isEqualTo("com.example");
}

@Test
void generatePackageNameReservedKeywordsEndOfPackageName() {
assertThat(this.properties.cleanPackageName("com.foo.null", "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName("com.foo.null", false, "com.example")).isEqualTo("com.example");
}

@Test
void generatePackageNameReservedKeywordsEntirePackageName() {
assertThat(this.properties.cleanPackageName("public", "com.example")).isEqualTo("com.example");
assertThat(this.properties.cleanPackageName("public", false, "com.example")).isEqualTo("com.example");
}

@Test
void generateKotlinPackageNameSimple() {
assertThat(this.properties.cleanPackageName("com.foo", true, "com.example")).isEqualTo("com.foo");
}

@Test
void generateKotlinPackageNameSimpleUnderscore() {
assertThat(this.properties.cleanPackageName("com.my_foo", true, "com.example")).isEqualTo("com.my_foo");
}

@Test
void generateKotlinPackageNameSimpleColon() {
assertThat(this.properties.cleanPackageName("com:foo", true, "com.example")).isEqualTo("com.foo");
}

@Test
void generateKotlinPackageNameMultipleDashes() {
assertThat(this.properties.cleanPackageName("com.foo--bar", true, "com.example")).isEqualTo("com.foo__bar");
}

@Test
void generateKotlinPackageNameMultipleSpaces() {
assertThat(this.properties.cleanPackageName(" com foo ", true, "com.example")).isEqualTo("com.foo");
}

@Test
void generateKotlinPackageNameNull() {
assertThat(this.properties.cleanPackageName(null, true, "com.example")).isEqualTo("com.example");
}

@Test
void generateKotlinPackageNameDot() {
assertThat(this.properties.cleanPackageName(".", true, "com.example")).isEqualTo("com.example");
}

@Test
void generateKotlinPackageNameWhitespaces() {
assertThat(this.properties.cleanPackageName(" ", true, "com.example")).isEqualTo("com.example");
}

@Test
void generateKotlinPackageNameInvalidStartCharacter() {
assertThat(this.properties.cleanPackageName("0com.foo", true, "com.example")).isEqualTo("_com.foo");
}

@Test
void generateKotlinPackageNameVersion() {
assertThat(this.properties.cleanPackageName("com.foo.test-1.4.5", true, "com.example"))
.isEqualTo("com.foo.test_145");
}

@Test
void generateKotlinPackageNameInvalidPackageName() {
assertThat(this.properties.cleanPackageName("org.springframework", true, "com.example"))
.isEqualTo("com.example");
}

@Test
void generateKotlinPackageNameReservedKeywordsMiddleOfPackageName() {
assertThat(this.properties.cleanPackageName("com.return.foo", true, "com.example")).isEqualTo("com.return.foo");
}

@Test
void generateKotlinPackageNameReservedKeywordsStartOfPackageName() {
assertThat(this.properties.cleanPackageName("false.com.foo", true, "com.example")).isEqualTo("false.com.foo");
}

@Test
void generateKotlinPackageNameReservedKeywordsEndOfPackageName() {
assertThat(this.properties.cleanPackageName("com.foo.null", true, "com.example")).isEqualTo("com.foo.null");
}

@Test
void generateKotlinPackageNameReservedChar() {
assertThat(this.properties.cleanPackageName("com._foo.null", true, "com.example")).isEqualTo("com._foo.null");
}

@Test
void generateKotlinPackageNameJavaReservedKeywords() {
assertThat(this.properties.cleanPackageName("public", true, "com.example")).isEqualTo("public");
}

@Test
void generateKotlinPackageNameJavaReservedKeywordsEntirePackageName() {
assertThat(this.properties.cleanPackageName("public.package", true, "com.example")).isEqualTo("public.package");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

package io.spring.initializr.web.project;

import java.util.Optional;
import java.util.function.Supplier;

import io.spring.initializr.generator.language.Language;
import io.spring.initializr.generator.language.kotlin.KotlinLanguage;
import io.spring.initializr.generator.project.MutableProjectDescription;
import io.spring.initializr.generator.project.ProjectDescriptionCustomizer;
import io.spring.initializr.generator.version.Version;
Expand Down Expand Up @@ -64,8 +67,14 @@ public void customize(MutableProjectDescription description) {
else if (targetArtifactId.equals(description.getName())) {
description.setName(cleanMavenCoordinate(targetArtifactId, "-"));
}

boolean isKotlin = Optional.ofNullable(description.getLanguage())
.map(Language::id)
.filter((id) -> id.equals(KotlinLanguage.ID))
.isPresent();

description.setPackageName(this.metadata.getConfiguration()
.cleanPackageName(description.getPackageName(), this.metadata.getPackageName().getContent()));
.cleanPackageName(description.getPackageName(), isKotlin, this.metadata.getPackageName().getContent()));
if (description.getPlatformVersion() == null) {
description.setPlatformVersion(Version.parse(this.metadata.getBootVersions().getDefault().getId()));
}
Expand Down

0 comments on commit 776e662

Please sign in to comment.