Skip to content

Commit

Permalink
Polish "Escape keywords in kotlin package declarations"
Browse files Browse the repository at this point in the history
  • Loading branch information
mhalbritter committed Aug 7, 2024
1 parent 776e662 commit bce08cf
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 73 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,16 +56,12 @@ 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 Language language;

private final IndentingWriterFactory indentingWriterFactory;

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

Expand All @@ -75,12 +72,6 @@ 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());
Expand Down Expand Up @@ -140,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
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,12 +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 isKotlin if the package name clean is for kotlin project
* @param language the project language
* @param defaultPackageName the default package name
* @return the cleaned package name
* @see Env#getInvalidPackageNames()
*/
public String cleanPackageName(String packageName, boolean isKotlin, String defaultPackageName) {
public String cleanPackageName(String packageName, Language language, String defaultPackageName) {
if (!StringUtils.hasText(packageName)) {
return defaultPackageName;
}
Expand All @@ -119,14 +118,16 @@ public String cleanPackageName(String packageName, boolean isKotlin, String defa
if (hasInvalidChar(candidate.replace(".", "")) || this.env.invalidPackageNames.contains(candidate)) {
return defaultPackageName;
}

// No check for Kotlin as its reserved keywords will be escaped later
if (!isKotlin && 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 @@ -168,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 bce08cf

Please sign in to comment.