From 776e662aa4798a8a7c487860a3ad14b3fd5f3547 Mon Sep 17 00:00:00 2001 From: Ant00000ny Date: Tue, 16 Jul 2024 13:48:29 +0800 Subject: [PATCH] Escape keywords in kotlin package declarations See gh-1555 --- .../kotlin/KotlinSourceCodeWriter.java | 15 ++- .../kotlin/KotlinSourceCodeWriterTests.java | 48 +++++++ .../metadata/InitializrConfiguration.java | 7 +- .../InitializrConfigurationTests.java | 119 +++++++++++++++--- .../MetadataProjectDescriptionCustomizer.java | 11 +- 5 files changed, 181 insertions(+), 19 deletions(-) diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java index b156576c8e..b9c86753ff 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java @@ -55,6 +55,13 @@ public class KotlinSourceCodeWriter implements SourceCodeWriter 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) { @@ -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 imports = determineImports(compilationUnit); if (!imports.isEmpty()) { diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java index 46ce49c6f2..77a6a76d4a 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java @@ -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 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 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 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 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 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 lines = writeSingleType(sourceCode, "com/example/package/Test.kt"); + assertThat(lines).containsExactly("package com.example.`package`"); + } + private List writeSingleType(KotlinSourceCode sourceCode, String location) throws IOException { Path source = writeSourceCode(sourceCode).resolve(location); try (InputStream stream = Files.newInputStream(source)) { diff --git a/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java b/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java index 2a2e2c0d30..7099141939 100644 --- a/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java +++ b/initializr-metadata/src/main/java/io/spring/initializr/metadata/InitializrConfiguration.java @@ -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; } @@ -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 { diff --git a/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrConfigurationTests.java b/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrConfigurationTests.java index 5ff9bf096f..e02309c5b5 100755 --- a/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrConfigurationTests.java +++ b/initializr-metadata/src/test/java/io/spring/initializr/metadata/InitializrConfigurationTests.java @@ -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 diff --git a/initializr-web/src/main/java/io/spring/initializr/web/project/MetadataProjectDescriptionCustomizer.java b/initializr-web/src/main/java/io/spring/initializr/web/project/MetadataProjectDescriptionCustomizer.java index 9ffa6d722c..1e1dda6552 100644 --- a/initializr-web/src/main/java/io/spring/initializr/web/project/MetadataProjectDescriptionCustomizer.java +++ b/initializr-web/src/main/java/io/spring/initializr/web/project/MetadataProjectDescriptionCustomizer.java @@ -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; @@ -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())); }