Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generating a Kotlin project with a package name containing keywords will result in an unusable project #1555

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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