Skip to content

Commit

Permalink
Merge pull request #1555 from Ant00000ny
Browse files Browse the repository at this point in the history
* pr/1555:
  Polish "Escape keywords in kotlin package declarations"
  Escape keywords in kotlin package declarations

Closes gh-1555
  • Loading branch information
mhalbritter committed Aug 7, 2024
2 parents e3dbdd4 + bce08cf commit f5cdbd0
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,17 @@ public MainSourceCodeProjectContributor<KotlinTypeDeclaration, KotlinCompilation
ObjectProvider<MainCompilationUnitCustomizer<?, ?>> mainCompilationUnitCustomizers,
ObjectProvider<MainSourceCodeCustomizer<?, ?, ?>> mainSourceCodeCustomizers) {
return new MainSourceCodeProjectContributor<>(this.description, KotlinSourceCode::new,
new KotlinSourceCodeWriter(this.indentingWriterFactory), mainApplicationTypeCustomizers,
mainCompilationUnitCustomizers, mainSourceCodeCustomizers);
new KotlinSourceCodeWriter(this.description.getLanguage(), this.indentingWriterFactory),
mainApplicationTypeCustomizers, mainCompilationUnitCustomizers, mainSourceCodeCustomizers);
}

@Bean
public TestSourceCodeProjectContributor<KotlinTypeDeclaration, KotlinCompilationUnit, KotlinSourceCode> testKotlinSourceCodeProjectContributor(
ObjectProvider<TestApplicationTypeCustomizer<?>> testApplicationTypeCustomizers,
ObjectProvider<TestSourceCodeCustomizer<?, ?, ?>> testSourceCodeCustomizers) {
return new TestSourceCodeProjectContributor<>(this.description, KotlinSourceCode::new,
new KotlinSourceCodeWriter(this.indentingWriterFactory), testApplicationTypeCustomizers,
testSourceCodeCustomizers);
new KotlinSourceCodeWriter(this.description.getLanguage(), this.indentingWriterFactory),
testApplicationTypeCustomizers, testSourceCodeCustomizers);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
* A language in which a generated project can be written.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
public interface Language {

Expand All @@ -50,6 +51,19 @@ public interface Language {
*/
String sourceFileExtension();

/**
* Whether the language supports escaping keywords in package declarations.
* @return whether the language supports escaping keywords in package declarations.
*/
boolean supportsEscapingKeywordsInPackage();

/**
* Whether the given {@code input} is a keyword.
* @param input the input
* @return whether the input is a keyword
*/
boolean isKeyword(String input);

static Language forId(String id, String jvmVersion) {
return SpringFactoriesLoader.loadFactories(LanguageFactory.class, LanguageFactory.class.getClassLoader())
.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@

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

import java.util.Set;

import io.spring.initializr.generator.language.AbstractLanguage;
import io.spring.initializr.generator.language.Language;

/**
* Groovy {@link Language}.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
public final class GroovyLanguage extends AbstractLanguage {

// See https://docs.groovy-lang.org/latest/html/documentation/#_keywords
private static final Set<String> KEYWORDS = Set.of("abstract", "assert", "break", "case", "catch", "class", "const",
"continue", "def", "default", "do", "else", "enum", "extends", "final", "finally", "for", "goto", "if",
"implements", "import", "instanceof", "interface", "native", "new", "null", "non-sealed", "package",
"public", "protected", "private", "return", "static", "strictfp", "super", "switch", "synchronized", "this",
"threadsafe", "throw", "throws", "transient", "try", "while");

/**
* Groovy {@link Language} identifier.
*/
Expand All @@ -39,4 +49,14 @@ public GroovyLanguage(String jvmVersion) {
super(ID, jvmVersion, "groovy");
}

@Override
public boolean supportsEscapingKeywordsInPackage() {
return false;
}

@Override
public boolean isKeyword(String input) {
return KEYWORDS.contains(input);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

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

import javax.lang.model.SourceVersion;

import io.spring.initializr.generator.language.AbstractLanguage;
import io.spring.initializr.generator.language.Language;

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

@Override
public boolean supportsEscapingKeywordsInPackage() {
return false;
}

@Override
public boolean isKeyword(String input) {
return SourceVersion.isKeyword(input);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,26 @@

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

import java.util.Set;

import io.spring.initializr.generator.language.AbstractLanguage;
import io.spring.initializr.generator.language.Language;

/**
* Kotlin {@link Language}.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
public final class KotlinLanguage extends AbstractLanguage {

// 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> 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");

/**
* Kotlin {@link Language} identifier.
*/
Expand All @@ -39,4 +49,14 @@ public KotlinLanguage(String jvmVersion) {
super(ID, jvmVersion, "kt");
}

@Override
public boolean supportsEscapingKeywordsInPackage() {
return true;
}

@Override
public boolean isKeyword(String input) {
return KEYWORDS.contains(input);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import io.spring.initializr.generator.language.CodeBlock;
import io.spring.initializr.generator.language.CodeBlock.FormattingOptions;
import io.spring.initializr.generator.language.CompilationUnit;
import io.spring.initializr.generator.language.Language;
import io.spring.initializr.generator.language.Parameter;
import io.spring.initializr.generator.language.SourceCode;
import io.spring.initializr.generator.language.SourceCodeWriter;
Expand All @@ -55,9 +56,12 @@ public class KotlinSourceCodeWriter implements SourceCodeWriter<KotlinSourceCode

private static final FormattingOptions FORMATTING_OPTIONS = new KotlinFormattingOptions();

private final Language language;

private final IndentingWriterFactory indentingWriterFactory;

public KotlinSourceCodeWriter(IndentingWriterFactory indentingWriterFactory) {
public KotlinSourceCodeWriter(Language language, IndentingWriterFactory indentingWriterFactory) {
this.language = language;
this.indentingWriterFactory = indentingWriterFactory;
}

Expand All @@ -73,7 +77,7 @@ private void writeTo(SourceStructure structure, KotlinCompilationUnit compilatio
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 Expand Up @@ -127,6 +131,12 @@ private void writeTo(SourceStructure structure, KotlinCompilationUnit compilatio
}
}

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

private void writeProperty(IndentingWriter writer, KotlinPropertyDeclaration propertyDeclaration) {
writer.println();
writeModifiers(writer, propertyDeclaration.getModifiers());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class KotlinSourceCodeWriterTests {
@TempDir
Path directory;

private final KotlinSourceCodeWriter writer = new KotlinSourceCodeWriter(
private final KotlinSourceCodeWriter writer = new KotlinSourceCodeWriter(new KotlinLanguage(),
IndentingWriterFactory.withDefaultSettings());

@Test
Expand Down 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 @@ -25,9 +25,8 @@
import java.util.List;
import java.util.Map;

import javax.lang.model.SourceVersion;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.spring.initializr.generator.language.Language;
import io.spring.initializr.generator.version.InvalidVersionException;
import io.spring.initializr.generator.version.Version;
import io.spring.initializr.generator.version.Version.Format;
Expand Down Expand Up @@ -103,11 +102,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 language the project language
* @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, Language language, String defaultPackageName) {
if (!StringUtils.hasText(packageName)) {
return defaultPackageName;
}
Expand All @@ -118,12 +118,16 @@ public String cleanPackageName(String packageName, String defaultPackageName) {
if (hasInvalidChar(candidate.replace(".", "")) || this.env.invalidPackageNames.contains(candidate)) {
return defaultPackageName;
}
if (hasReservedKeyword(candidate)) {
return defaultPackageName;
}
else {
return candidate;
if (!supportsEscapingKeywordsInPackage(language)) {
if (hasReservedKeyword(language, candidate)) {
return defaultPackageName;
}
}
return candidate;
}

private boolean supportsEscapingKeywordsInPackage(Language language) {
return (language != null) ? language.supportsEscapingKeywordsInPackage() : false;
}

static String cleanPackageName(String packageName) {
Expand Down Expand Up @@ -165,8 +169,11 @@ private static boolean hasInvalidChar(String text) {
return false;
}

private static boolean hasReservedKeyword(final String packageName) {
return Arrays.stream(packageName.split("\\.")).anyMatch(SourceVersion::isKeyword);
private static boolean hasReservedKeyword(Language language, String packageName) {
if (language == null) {
return false;
}
return Arrays.stream(packageName.split("\\.")).anyMatch(language::isKeyword);
}

/**
Expand Down
Loading

0 comments on commit f5cdbd0

Please sign in to comment.