diff --git a/.gitignore b/.gitignore index b68ac0242..bbf1de64f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ hs_err_pid* **/.gradle/ .sandbox/ +.intellijPlatform/ +.kotlin/ /gen/ diff --git a/changelog.md b/changelog.md index a57d8ded1..70d25386c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,6 @@ # Minecraft Development for IntelliJ -## [Unreleased] +## [1.8.1] - 2024-08-10 ### Added @@ -11,6 +11,13 @@ - Inspection highlighting discouraged instruction shifts - Inspections for when @Inject local capture is unused and for when they can be replaced with @Local - [#2306](https://github.com/minecraft-dev/MinecraftDev/issues/2306) Use mixin icon for mixin classes +- Documentation while completing keys in mods.toml +- mods.toml support for neoforge.mods.toml +- Automatically insert an `=` after completing a mods.toml key + +### Changed + +- [#2353](https://github.com/minecraft-dev/MinecraftDev/issues/2353) Move "Method must not be static" error message to static keyword ([#2354](https://github.com/minecraft-dev/MinecraftDev/pull/2354)) ### Fixed @@ -20,8 +27,10 @@ - [#2163](https://github.com/minecraft-dev/MinecraftDev/issues/2163) `@ModifyVariable` method signature checking with `STORE` - [#2282](https://github.com/minecraft-dev/MinecraftDev/issues/2282) Mixin support confusion with `$` and `.` separators in class names - Recent NeoModDev version import errors +- Recommended Artifact ID value was not sanitized properly +- NeoForge versions in the Architectury were not being matched correctly for the first version of a major Minecraft release -## [1.8.0] +## [1.8.0] - 2024-07-14 This release contains two major features: - Support for MixinExtras expressions ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) diff --git a/gradle.properties b/gradle.properties index 93f97d634..d95ed857e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ kotlin.code.style=official ideaVersion = 2024.1 ideaVersionName = 2024.1 -coreVersion = 1.8.0 +coreVersion = 1.8.1 downloadIdeaSources = true pluginTomlVersion = 241.14494.150 diff --git a/src/main/kotlin/creator/custom/CreatorContext.kt b/src/main/kotlin/creator/custom/CreatorContext.kt index 61f8b160f..f22f79c37 100644 --- a/src/main/kotlin/creator/custom/CreatorContext.kt +++ b/src/main/kotlin/creator/custom/CreatorContext.kt @@ -54,4 +54,7 @@ data class CreatorContext( * A general purpose scope dependent of the main creator scope, cancelled when the creator is closed. */ fun childScope(name: String): CoroutineScope = scope.namedChildScope(name) + + @Suppress("UNCHECKED_CAST") + fun property(name: String): CreatorProperty = properties[name] as CreatorProperty } diff --git a/src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt b/src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt new file mode 100644 index 000000000..3fa6c1cd8 --- /dev/null +++ b/src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt @@ -0,0 +1,366 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.MCDevBundle.invoke +import com.demonwav.mcdev.creator.custom.finalizers.CreatorFinalizer +import com.demonwav.mcdev.creator.custom.providers.EmptyLoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.creator.custom.types.CreatorPropertyFactory +import com.demonwav.mcdev.creator.custom.types.ExternalCreatorProperty +import com.demonwav.mcdev.util.toTypedArray +import com.demonwav.mcdev.util.virtualFileOrError +import com.intellij.codeInsight.CodeInsightSettings +import com.intellij.codeInsight.actions.ReformatCodeProcessor +import com.intellij.ide.projectView.ProjectView +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.refreshAndFindVirtualFile +import com.intellij.psi.PsiManager +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.application +import java.nio.file.Path +import java.util.function.Consumer +import kotlin.collections.mapNotNull +import kotlin.collections.orEmpty +import kotlin.collections.set +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText +import kotlinx.coroutines.CoroutineScope + +interface ExternalTemplatePropertyProvider { + + val projectNameProperty: GraphProperty + + val useGit: Boolean +} + +/** + * Handles all the logic involved in making the creator UI and generating the project files. + */ +class CreatorTemplateProcessor( + propertyGraph: PropertyGraph, + wizardContext: WizardContext, + scope: CoroutineScope, + private val externalPropertyProvider: ExternalTemplatePropertyProvider, +) { + + var hasTemplateErrors: Boolean = true + private set + + private var properties: MutableMap> = mutableMapOf() + var context: CreatorContext = CreatorContext(propertyGraph, properties, wizardContext, scope) + + fun initBuiltinProperties() { + val projectNameProperty = externalPropertyProvider.projectNameProperty + properties["PROJECT_NAME"] = ExternalCreatorProperty( + context = context, + graphProperty = projectNameProperty, + valueType = String::class.java + ) + } + + fun createOptionsPanel(template: LoadedTemplate): DialogPanel? { + properties = mutableMapOf() + context = context.copy(properties = properties) + + if (!template.isValid) { + return null + } + + initBuiltinProperties() + + return panel { + val reporter = TemplateValidationReporterImpl() + val uiFactories = setupTemplate(template, reporter) + if (uiFactories.isEmpty() && !reporter.hasErrors) { + row { + label(MCDevBundle("creator.ui.warn.no_properties")) + .component.foreground = JBColor.YELLOW + } + } else { + hasTemplateErrors = reporter.hasErrors + reporter.display(this) + + if (!reporter.hasErrors) { + for (uiFactory in uiFactories) { + uiFactory.accept(this) + } + } + } + } + } + + fun setupTemplate( + template: LoadedTemplate, + reporter: TemplateValidationReporterImpl + ): List> { + return try { + val properties = template.descriptor.properties.orEmpty() + .mapNotNull { + reporter.subject = it.name + setupProperty(it, reporter) + } + .sortedBy { (_, order) -> order } + .map { it.first } + + val finalizers = template.descriptor.finalizers + if (finalizers != null) { + CreatorFinalizer.validateAll(reporter, finalizers) + } + + properties + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + if (t is TemplateValidationException && application.isUnitTestMode) { + // Rethrowing makes the exception properly catchable while not polluting the test outputs + throw t + } + + thisLogger().error( + "Unexpected error during template setup", + t, + template.label, + template.descriptor.toString() + ) + + emptyList() + } finally { + reporter.subject = null + } + } + + private fun setupProperty( + descriptor: TemplatePropertyDescriptor, + reporter: TemplateValidationReporter + ): Pair, Int>? { + if (!descriptor.groupProperties.isNullOrEmpty()) { + val childrenUiFactories = descriptor.groupProperties + .mapNotNull { setupProperty(it, reporter) } + .sortedBy { (_, order) -> order } + .map { it.first } + + val factory = Consumer { panel -> + val label = descriptor.translatedLabel + if (descriptor.collapsible == false) { + panel.group(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@group) + } + } + } else { + val group = panel.collapsibleGroup(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@collapsibleGroup) + } + } + + group.expanded = descriptor.default as? Boolean ?: false + } + } + + val order = descriptor.order ?: 0 + return factory to order + } + + if (descriptor.name in properties.keys) { + reporter.fatal("Duplicate property name ${descriptor.name}") + } + + val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, context) + if (prop == null) { + reporter.fatal("Unknown template property type ${descriptor.type}") + } + + prop.setupProperty(reporter) + + properties[descriptor.name] = prop + + if (descriptor.visible == false) { + return null + } + + val factory = Consumer { panel -> prop.buildUi(panel) } + val order = descriptor.order ?: 0 + return factory to order + } + + fun generateFiles(project: Project, template: LoadedTemplate) { + if (template is EmptyLoadedTemplate) { + return + } + + val projectPath = context.wizardContext.projectDirectory + val templateProperties = collectTemplateProperties() + thisLogger().debug("Template properties: $templateProperties") + + val generatedFiles = mutableListOf>() + for (file in template.descriptor.files.orEmpty()) { + if (file.condition != null && + !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } + ) { + continue + } + + val relativeTemplate = TemplateEvaluator.template(templateProperties, file.template).getOrNull() + ?: continue + val relativeDest = TemplateEvaluator.template(templateProperties, file.destination).getOrNull() + ?: continue + + try { + val templateContents = template.loadTemplateContents(relativeTemplate) + ?: continue + + val destPath = projectPath.resolve(relativeDest).toAbsolutePath() + if (!destPath.startsWith(projectPath)) { + // We want to make sure template files aren't 'escaping' the project directory + continue + } + + var fileTemplateProperties = templateProperties + if (file.properties != null) { + fileTemplateProperties = templateProperties.toMutableMap() + fileTemplateProperties.putAll(file.properties) + } + + val processedContent = TemplateEvaluator.template(fileTemplateProperties, templateContents) + .onFailure { t -> + val attachment = Attachment(relativeTemplate, templateContents) + thisLogger().error("Failed evaluate template '$relativeTemplate'", t, attachment) + } + .getOrNull() + ?: continue + + destPath.parent.createDirectories() + destPath.writeText(processedContent) + + val virtualFile = destPath.refreshAndFindVirtualFile() + if (virtualFile != null) { + generatedFiles.add(file to virtualFile) + } else { + thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") + } + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to process template file $file", t) + } + } + + val finalizeAction = { + WriteAction.runAndWait { + LocalFileSystem.getInstance().refresh(false) + // Apparently a module root is required for the reformat to work + setupTempRootModule(project, projectPath) + + reformatFiles(project, generatedFiles) + openFilesInEditor(project, generatedFiles) + } + + val finalizers = template.descriptor.finalizers + if (!finalizers.isNullOrEmpty()) { + CreatorFinalizer.executeAll(context.wizardContext, project, finalizers, templateProperties) + } + } + if (context.wizardContext.isCreatingNewProject) { + TemplateService.instance.registerFinalizerAction(project, finalizeAction) + } else { + application.executeOnPooledThread { finalizeAction() } + } + } + + private fun setupTempRootModule(project: Project, projectPath: Path) { + val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() + val module = modifiableModel.newNonPersistentModule("mcdev-temp-root", ModuleTypeId.JAVA_MODULE) + val rootsModel = ModuleRootManager.getInstance(module).modifiableModel + rootsModel.addContentEntry(projectPath.virtualFileOrError) + rootsModel.commit() + modifiableModel.commit() + } + + private fun collectTemplateProperties(): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + into["USE_GIT"] = externalPropertyProvider.useGit + + return properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } + + private fun reformatFiles( + project: Project, + files: MutableList> + ) { + val psiManager = PsiManager.getInstance(project) + val psiFiles = files.asSequence() + .filter { (desc, _) -> desc.reformat != false } + .mapNotNull { (_, file) -> psiManager.findFile(file) } + + val processor = ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false) + psiFiles.forEach(processor::setDoNotKeepLineBreaks) + + val insightSettings = CodeInsightSettings.getInstance() + val oldSecondReformat = insightSettings.ENABLE_SECOND_REFORMAT + insightSettings.ENABLE_SECOND_REFORMAT = true + try { + processor.run() + } finally { + insightSettings.ENABLE_SECOND_REFORMAT = oldSecondReformat + } + } + + private fun openFilesInEditor( + project: Project, + files: MutableList> + ) { + val fileEditorManager = FileEditorManager.getInstance(project) + val projectView = ProjectView.getInstance(project) + for ((desc, file) in files) { + if (desc.openInEditor == true) { + fileEditorManager.openFile(file, true) + projectView.select(null, file, false) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index 73517b8c6..baccb9175 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -22,43 +22,23 @@ package com.demonwav.mcdev.creator.custom import com.demonwav.mcdev.MinecraftSettings import com.demonwav.mcdev.asset.MCDevBundle -import com.demonwav.mcdev.creator.custom.finalizers.CreatorFinalizer import com.demonwav.mcdev.creator.custom.providers.EmptyLoadedTemplate import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate import com.demonwav.mcdev.creator.custom.providers.TemplateProvider -import com.demonwav.mcdev.creator.custom.types.CreatorProperty -import com.demonwav.mcdev.creator.custom.types.CreatorPropertyFactory -import com.demonwav.mcdev.creator.custom.types.ExternalCreatorProperty import com.demonwav.mcdev.creator.modalityState -import com.demonwav.mcdev.util.toTypedArray -import com.demonwav.mcdev.util.virtualFileOrError -import com.intellij.codeInsight.CodeInsightSettings -import com.intellij.codeInsight.actions.ReformatCodeProcessor -import com.intellij.ide.projectView.ProjectView import com.intellij.ide.wizard.AbstractNewProjectWizardStep import com.intellij.ide.wizard.GitNewProjectWizardData import com.intellij.ide.wizard.NewProjectWizardBaseData import com.intellij.ide.wizard.NewProjectWizardStep import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.WriteAction import com.intellij.openapi.application.asContextElement -import com.intellij.openapi.diagnostic.Attachment -import com.intellij.openapi.diagnostic.ControlFlowException import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.module.ModuleManager -import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.observable.properties.GraphProperty import com.intellij.openapi.observable.util.transform import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.refreshAndFindVirtualFile -import com.intellij.psi.PsiManager import com.intellij.ui.JBColor import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Cell @@ -67,17 +47,11 @@ import com.intellij.ui.dsl.builder.Placeholder import com.intellij.ui.dsl.builder.SegmentedButton import com.intellij.ui.dsl.builder.TopGap import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel import com.intellij.util.application import com.intellij.util.ui.AsyncProcessIcon -import java.nio.file.Path -import java.util.function.Consumer import javax.swing.JLabel import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.set -import kotlin.io.path.createDirectories -import kotlin.io.path.writeText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -124,10 +98,16 @@ class CustomPlatformStep( lateinit var noTemplatesAvailable: Cell var templateLoadingJob: Job? = null - private var hasTemplateErrors: Boolean = true + private val externalPropertyProvider = object : ExternalTemplatePropertyProvider { + override val projectNameProperty: GraphProperty + get() = data.getUserData(NewProjectWizardBaseData.KEY)?.nameProperty + ?: throw RuntimeException("Could not find wizard base data") - private var properties = mutableMapOf>() - private var creatorContext = CreatorContext(propertyGraph, properties, context, creatorScope) + override val useGit: Boolean + get() = data.getUserData(GitNewProjectWizardData.KEY)?.git == true + } + private val templateProcessor = + CreatorTemplateProcessor(propertyGraph, context, creatorScope, externalPropertyProvider) init { Disposer.register(context.disposable) { @@ -178,7 +158,7 @@ class CustomPlatformStep( toolTipText = template.tooltip }.bind(selectedTemplateProperty) .validation { - addApplyRule("", condition = ::hasTemplateErrors) + addApplyRule("", condition = templateProcessor::hasTemplateErrors) } }.visibleIf( availableTemplatesProperty.transform { it.size > 1 } @@ -203,7 +183,7 @@ class CustomPlatformStep( } selectedTemplateProperty.afterChange { template -> - createOptionsPanelInBackground(template, templatePropertyPlaceholder) + templatePropertyPlaceholder.component = templateProcessor.createOptionsPanel(template) } builder.row { @@ -290,276 +270,7 @@ class CustomPlatformStep( } } - private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { - properties = mutableMapOf() - creatorContext = creatorContext.copy(properties = properties) - - if (!template.isValid) { - return - } - - val baseData = data.getUserData(NewProjectWizardBaseData.KEY) - ?: return thisLogger().error("Could not find wizard base data") - - properties["PROJECT_NAME"] = ExternalCreatorProperty( - context = creatorContext, - graphProperty = baseData.nameProperty, - valueType = String::class.java - ) - - placeholder.component = panel { - val reporter = TemplateValidationReporterImpl() - val uiFactories = setupTemplate(template, reporter) - if (uiFactories.isEmpty() && !reporter.hasErrors) { - row { - label(MCDevBundle("creator.ui.warn.no_properties")) - .component.foreground = JBColor.YELLOW - } - } else { - hasTemplateErrors = reporter.hasErrors - reporter.display(this) - - if (!reporter.hasErrors) { - for (uiFactory in uiFactories) { - uiFactory.accept(this) - } - } - } - } - } - - private fun setupTemplate( - template: LoadedTemplate, - reporter: TemplateValidationReporterImpl - ): List> { - return try { - val properties = template.descriptor.properties.orEmpty() - .mapNotNull { - reporter.subject = it.name - setupProperty(it, reporter) - } - .sortedBy { (_, order) -> order } - .map { it.first } - - val finalizers = template.descriptor.finalizers - if (finalizers != null) { - CreatorFinalizer.validateAll(reporter, finalizers) - } - - properties - } catch (t: Throwable) { - if (t is ControlFlowException) { - throw t - } - - thisLogger().error( - "Unexpected error during template setup", - t, - template.label, - template.descriptor.toString() - ) - - emptyList() - } finally { - reporter.subject = null - } - } - - private fun setupProperty( - descriptor: TemplatePropertyDescriptor, - reporter: TemplateValidationReporter - ): Pair, Int>? { - if (!descriptor.groupProperties.isNullOrEmpty()) { - val childrenUiFactories = descriptor.groupProperties - .mapNotNull { setupProperty(it, reporter) } - .sortedBy { (_, order) -> order } - .map { it.first } - - val factory = Consumer { panel -> - val label = descriptor.translatedLabel - if (descriptor.collapsible == false) { - panel.group(label) { - for (childFactory in childrenUiFactories) { - childFactory.accept(this@group) - } - } - } else { - val group = panel.collapsibleGroup(label) { - for (childFactory in childrenUiFactories) { - childFactory.accept(this@collapsibleGroup) - } - } - - group.expanded = descriptor.default as? Boolean ?: false - } - } - - val order = descriptor.order ?: 0 - return factory to order - } - - if (descriptor.name in properties.keys) { - reporter.fatal("Duplicate property name ${descriptor.name}") - } - - val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, creatorContext) - if (prop == null) { - reporter.fatal("Unknown template property type ${descriptor.type}") - } - - prop.setupProperty(reporter) - - properties[descriptor.name] = prop - - if (descriptor.visible == false) { - return null - } - - val factory = Consumer { panel -> prop.buildUi(panel) } - val order = descriptor.order ?: 0 - return factory to order - } - override fun setupProject(project: Project) { - val template = selectedTemplate - if (template is EmptyLoadedTemplate) { - return - } - - val projectPath = context.projectDirectory - val templateProperties = collectTemplateProperties() - thisLogger().debug("Template properties: $templateProperties") - - val generatedFiles = mutableListOf>() - for (file in template.descriptor.files.orEmpty()) { - if (file.condition != null && - !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } - ) { - continue - } - - val relativeTemplate = TemplateEvaluator.template(templateProperties, file.template).getOrNull() - ?: continue - val relativeDest = TemplateEvaluator.template(templateProperties, file.destination).getOrNull() - ?: continue - - try { - val templateContents = template.loadTemplateContents(relativeTemplate) - ?: continue - - val destPath = projectPath.resolve(relativeDest).toAbsolutePath() - if (!destPath.startsWith(projectPath)) { - // We want to make sure template files aren't 'escaping' the project directory - continue - } - - var fileTemplateProperties = templateProperties - if (file.properties != null) { - fileTemplateProperties = templateProperties.toMutableMap() - fileTemplateProperties.putAll(file.properties) - } - - val processedContent = TemplateEvaluator.template(fileTemplateProperties, templateContents) - .onFailure { t -> - val attachment = Attachment(relativeTemplate, templateContents) - thisLogger().error("Failed evaluate template '$relativeTemplate'", t, attachment) - } - .getOrNull() - ?: continue - - destPath.parent.createDirectories() - destPath.writeText(processedContent) - - val virtualFile = destPath.refreshAndFindVirtualFile() - if (virtualFile != null) { - generatedFiles.add(file to virtualFile) - } else { - thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") - } - } catch (t: Throwable) { - if (t is ControlFlowException) { - throw t - } - - thisLogger().error("Failed to process template file $file", t) - } - } - - val finalizeAction = { - WriteAction.runAndWait { - LocalFileSystem.getInstance().refresh(false) - // Apparently a module root is required for the reformat to work - setupTempRootModule(project, projectPath) - - reformatFiles(project, generatedFiles) - openFilesInEditor(project, generatedFiles) - } - - val finalizers = selectedTemplate.descriptor.finalizers - if (!finalizers.isNullOrEmpty()) { - CreatorFinalizer.executeAll(context, project, finalizers, templateProperties) - } - } - if (context.isCreatingNewProject) { - TemplateService.instance.registerFinalizerAction(project, finalizeAction) - } else { - application.executeOnPooledThread { finalizeAction() } - } - } - - private fun setupTempRootModule(project: Project, projectPath: Path) { - val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() - val module = modifiableModel.newNonPersistentModule("mcdev-temp-root", ModuleTypeId.JAVA_MODULE) - val rootsModel = ModuleRootManager.getInstance(module).modifiableModel - rootsModel.addContentEntry(projectPath.virtualFileOrError) - rootsModel.commit() - modifiableModel.commit() - } - - private fun collectTemplateProperties(): MutableMap { - val into = mutableMapOf() - - into.putAll(TemplateEvaluator.baseProperties) - - val gitData = data.getUserData(GitNewProjectWizardData.KEY) - into["USE_GIT"] = gitData?.git == true - - return properties.mapValuesTo(into) { (_, prop) -> prop.get() } - } - - private fun reformatFiles( - project: Project, - files: MutableList> - ) { - val psiManager = PsiManager.getInstance(project) - val psiFiles = files.asSequence() - .filter { (desc, _) -> desc.reformat != false } - .mapNotNull { (_, file) -> psiManager.findFile(file) } - - val processor = ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false) - psiFiles.forEach(processor::setDoNotKeepLineBreaks) - - val insightSettings = CodeInsightSettings.getInstance() - val oldSecondReformat = insightSettings.ENABLE_SECOND_REFORMAT - insightSettings.ENABLE_SECOND_REFORMAT = true - try { - processor.run() - } finally { - insightSettings.ENABLE_SECOND_REFORMAT = oldSecondReformat - } - } - - private fun openFilesInEditor( - project: Project, - files: MutableList> - ) { - val fileEditorManager = FileEditorManager.getInstance(project) - val projectView = ProjectView.getInstance(project) - for ((desc, file) in files) { - if (desc.openInEditor == true) { - fileEditorManager.openFile(file, true) - projectView.select(null, file, false) - } - } + templateProcessor.generateFiles(project, selectedTemplate) } } diff --git a/src/main/kotlin/creator/custom/TemplateValidationReporter.kt b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt index b953eb16e..68af3632a 100644 --- a/src/main/kotlin/creator/custom/TemplateValidationReporter.kt +++ b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt @@ -36,6 +36,7 @@ interface TemplateValidationReporter { class TemplateValidationReporterImpl : TemplateValidationReporter { private val validationItems: MutableMap> = linkedMapOf() + val items: Map> = validationItems var hasErrors = false private set var hasWarns = false @@ -99,7 +100,7 @@ class TemplateValidationReporterImpl : TemplateValidationReporter { class TemplateValidationException(message: String?, cause: Throwable? = null) : Exception(message, cause) -private sealed class TemplateValidationItem(val message: String, val color: JBColor) { +sealed class TemplateValidationItem(val message: String, val color: JBColor) { class Warn(message: String) : TemplateValidationItem(message, JBColor.YELLOW) class Error(message: String) : TemplateValidationItem(message, JBColor.RED) diff --git a/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt index 8d1aaf1dd..4a65ec6f5 100644 --- a/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt +++ b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt @@ -28,6 +28,11 @@ fun interface PreparedDerivation { fun derive(parentValues: List): Any? } +object UnknownDerivation : PreparedDerivation { + + override fun derive(parentValues: List): Any? = null +} + interface PropertyDerivationFactory { fun create( diff --git a/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt index c3688f9ad..f2f84dda8 100644 --- a/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt +++ b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt @@ -28,13 +28,19 @@ class ReplacePropertyDerivation( val regex: Regex, val replacement: String, val maxLength: Int?, + val lowercase: Boolean, ) : PreparedDerivation { override fun derive(parentValues: List): Any? { val projectName = parentValues.first() as? String ?: return null - val sanitized = projectName.lowercase().replace(regex, replacement) + var sanitized = projectName + if (lowercase) { + sanitized = sanitized.lowercase() + } + + sanitized = sanitized.replace(regex, replacement) if (maxLength != null && sanitized.length > maxLength) { return sanitized.substring(0, maxLength) } @@ -59,7 +65,7 @@ class ReplacePropertyDerivation( return null } - if (parents.size > 2) { + if (parents.size > 1) { reporter.warn("More than one parent defined") } @@ -88,7 +94,8 @@ class ReplacePropertyDerivation( } val maxLength = (derivation.parameters["maxLength"] as? Number)?.toInt() - return ReplacePropertyDerivation(regex, replacement, maxLength) + val lowercase = derivation.parameters["lowercase"] as? Boolean == true + return ReplacePropertyDerivation(regex, replacement, maxLength, lowercase) } } } diff --git a/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt index bb1ccc310..2d39b1a84 100644 --- a/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt @@ -89,7 +89,10 @@ class BuildSystemCoordinatesCreatorProperty( graphProperty.dependsOn(projectNameProperty, false) { val newProjectName = projectNameProperty.get() if (newProjectName is String) { - coords.copy(artifactId = newProjectName) + val sanitizedArtifactId = newProjectName.lowercase() + .replace(Regex("\\W+"), "-") + .removeSuffix("-") + coords.copy(artifactId = sanitizedArtifactId) } else { coords } diff --git a/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt index 96b592b94..c7c02a520 100644 --- a/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.SuggestClassNamePropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.UnknownDerivation import com.demonwav.mcdev.creator.custom.model.ClassFqn import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.dsl.builder.Panel @@ -63,7 +64,7 @@ class ClassFqnCreatorProperty( SuggestClassNamePropertyDerivation.create(reporter, parents, derives) } - else -> null + else -> UnknownDerivation } class Factory : CreatorPropertyFactory { diff --git a/src/main/kotlin/creator/custom/types/CreatorProperty.kt b/src/main/kotlin/creator/custom/types/CreatorProperty.kt index 122cb86c6..21ce7a58e 100644 --- a/src/main/kotlin/creator/custom/types/CreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/CreatorProperty.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.UnknownDerivation import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.observable.properties.GraphProperty @@ -145,17 +146,17 @@ abstract class CreatorProperty( } derivation = setupDerivation(reporter, descriptor.derives) - if (derivation == null) { + if (derivation == UnknownDerivation) { reporter.fatal("Unknown method derivation: ${descriptor.derives}") - } - - @Suppress("UNCHECKED_CAST") - graphProperty.set(derive(collectDerivationParentValues(reporter), descriptor.derives) as T) - for (parent in parents) { - val parentProperty = properties[parent]!! - graphProperty.dependsOn(parentProperty.graphProperty, descriptor.derives.whenModified != false) { - @Suppress("UNCHECKED_CAST") - derive(collectDerivationParentValues(), descriptor.derives) as T + } else if (derivation != null) { + @Suppress("UNCHECKED_CAST") + graphProperty.set(derive(collectDerivationParentValues(reporter), descriptor.derives) as T) + for (parent in parents) { + val parentProperty = properties[parent]!! + graphProperty.dependsOn(parentProperty.graphProperty, descriptor.derives.whenModified != false) { + @Suppress("UNCHECKED_CAST") + derive(collectDerivationParentValues(), descriptor.derives) as T + } } } } @@ -173,10 +174,13 @@ abstract class CreatorProperty( } } + /** + * @return [UnknownDerivation] if the derivation method is unknown, `null` if the derivation is invalid. + */ protected open fun setupDerivation( reporter: TemplateValidationReporter, derives: PropertyDerivation - ): PreparedDerivation? = null + ): PreparedDerivation? = UnknownDerivation protected fun makeStorageKey(discriminator: String? = null): String { val base = "${javaClass.name}.property.${descriptor.name}.${descriptor.type}" diff --git a/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt index a0d849aef..126ad5f76 100644 --- a/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.RecommendJavaVersionForMcVersionPropertyDerivation import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.UnknownDerivation import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindIntText @@ -67,7 +68,7 @@ class IntegerCreatorProperty( SelectPropertyDerivation.create(reporter, emptyList(), derives) } - else -> null + else -> UnknownDerivation } class Factory : CreatorPropertyFactory { diff --git a/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt index 0369fd952..8cde96fe0 100644 --- a/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.ExtractVersionMajorMinorPropertyDerivation import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.UnknownDerivation import com.demonwav.mcdev.util.SemanticVersion import com.intellij.ui.dsl.builder.COLUMNS_SHORT import com.intellij.ui.dsl.builder.Panel @@ -67,7 +68,7 @@ open class SemanticVersionCreatorProperty( SelectPropertyDerivation.create(reporter, emptyList(), derives) } - else -> null + else -> UnknownDerivation } override fun convertSelectDerivationResult(original: Any?): Any? { diff --git a/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt index 675a1e4cf..827d63259 100644 --- a/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt @@ -28,6 +28,7 @@ import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.ReplacePropertyDerivation import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.demonwav.mcdev.creator.custom.derivation.UnknownDerivation import com.intellij.openapi.observable.properties.GraphProperty import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.dsl.builder.Panel @@ -77,7 +78,7 @@ class StringCreatorProperty( SelectPropertyDerivation.create(reporter, emptyList(), derives) } - else -> null + else -> UnknownDerivation } override fun buildSimpleUi(panel: Panel) { diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index bc168040a..4be5c9593 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.platform.mixin.util.isAssignable import com.demonwav.mcdev.platform.mixin.util.isConstructor import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter +import com.demonwav.mcdev.util.findKeyword import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.invokeLater import com.demonwav.mcdev.util.synchronize @@ -133,7 +134,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } else if (!shouldBeStatic && modifiers.hasModifierProperty(PsiModifier.STATIC)) { reportedStatic = true holder.registerProblem( - identifier, + modifiers.findKeyword(PsiModifier.STATIC) ?: identifier, "Method must not be static", QuickFixFactory.getInstance().createModifierListFix( modifiers, diff --git a/src/main/kotlin/platform/neoforge/version/NeoForgeVersion.kt b/src/main/kotlin/platform/neoforge/version/NeoForgeVersion.kt index e1b6427be..ad8aafb4d 100644 --- a/src/main/kotlin/platform/neoforge/version/NeoForgeVersion.kt +++ b/src/main/kotlin/platform/neoforge/version/NeoForgeVersion.kt @@ -48,7 +48,11 @@ class NeoForgeVersion private constructor(val versions: List) { fun getNeoForgeVersions(mcVersion: SemanticVersion): List { val versionText = mcVersion.toString() // Drop the 1. part of the mc version - val shortMcVersion = versionText.substringAfter('.') + var shortMcVersion = versionText.substringAfter('.') + if (!shortMcVersion.contains('.')) { + // Ensure we have the .0 part + shortMcVersion = "$shortMcVersion.0" + } val toList = versions.asSequence() .filter { it.substringBeforeLast('.') == shortMcVersion } .mapNotNull(SemanticVersion::tryParse) diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index e1abcbe96..6e09278c6 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -72,8 +72,8 @@ creator.ui.update_url.label=Update URL: creator.ui.depend.label=Depend: creator.ui.log_prefix.label=Log Prefix: creator.ui.load_at.label=Load At: -creator.ui.load_at.option.startup=Startup: -creator.ui.load_at.option.postworld=Post World: +creator.ui.load_at.option.startup=Startup +creator.ui.load_at.option.postworld=Post World creator.ui.soft_depend.label=Soft Depend: creator.ui.use_mixins.label=Use &Mixins: creator.ui.split_sources.label=Split Sources: diff --git a/src/test/kotlin/creator/CreatorTemplateProcessorTest.kt b/src/test/kotlin/creator/CreatorTemplateProcessorTest.kt new file mode 100644 index 000000000..5b8733a18 --- /dev/null +++ b/src/test/kotlin/creator/CreatorTemplateProcessorTest.kt @@ -0,0 +1,78 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@DisplayName("Creator Template Processor Tests") +class CreatorTemplateProcessorTest : CreatorTemplateProcessorTestBase() { + + @Test + @DisplayName("Duplicate Property Name") + fun duplicatePropertyName() { + val exception = assertThrows { + makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "PROP", + "type": "string" + }, + { + "name": "PROP", + "type": "string" + } + ] + } + """.trimIndent() + ) + } + assertEquals("Duplicate property name PROP", exception.message?.replace("\r\n", "\n")) + } + + @Test + @DisplayName("Unknown Property Type") + fun unknownPropertyType() { + val exception = assertThrows { + makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "PROP", + "type": "bad_type" + } + ] + } + """.trimIndent() + ) + } + assertEquals("Unknown template property type bad_type", exception.message) + } +} diff --git a/src/test/kotlin/creator/CreatorTemplateProcessorTestBase.kt b/src/test/kotlin/creator/CreatorTemplateProcessorTestBase.kt new file mode 100644 index 000000000..010661d29 --- /dev/null +++ b/src/test/kotlin/creator/CreatorTemplateProcessorTestBase.kt @@ -0,0 +1,92 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator + +import com.demonwav.mcdev.creator.custom.CreatorTemplateProcessor +import com.demonwav.mcdev.creator.custom.ExternalTemplatePropertyProvider +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.custom.TemplateService +import com.demonwav.mcdev.creator.custom.TemplateValidationReporterImpl +import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate +import com.google.gson.Gson +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.testFramework.fixtures.BareTestFixture +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach + +class TestLoadedTemplate(descriptor: String) : LoadedTemplate { + + override val label: String = "Test template" + + override val tooltip: String? = null + + override val descriptor: TemplateDescriptor = + Gson().fromJson(descriptor, TemplateDescriptor::class.java) + + override val isValid: Boolean = true + + override fun loadTemplateContents(path: String): String? { + return null + } +} + +class TestExternalTemplatePropertyProvider(propertyGraph: PropertyGraph) : ExternalTemplatePropertyProvider { + + override val projectNameProperty: GraphProperty = propertyGraph.property("Test project") + + override val useGit: Boolean = false +} + +abstract class CreatorTemplateProcessorTestBase { + + lateinit var fixture: BareTestFixture + lateinit var propertyGraph: PropertyGraph + lateinit var processor: CreatorTemplateProcessor + + @BeforeEach + open fun setUp() { + fixture = IdeaTestFixtureFactory.getFixtureFactory().createBareFixture() + fixture.setUp() + + propertyGraph = PropertyGraph() + val wizardContext = WizardContext(null, fixture.testRootDisposable) + val scope = TemplateService.instance.scope("MinecraftDev Creator Test") + val externalPropertyProvider = TestExternalTemplatePropertyProvider(propertyGraph) + processor = CreatorTemplateProcessor(propertyGraph, wizardContext, scope, externalPropertyProvider) + processor.initBuiltinProperties() + } + + @AfterEach + open fun tearDown() { + fixture.tearDown() + } + + fun makeTemplate(@Language("JSON") descriptor: String): TemplateValidationReporterImpl { + val reporter = TemplateValidationReporterImpl() + val loadedTemplate = TestLoadedTemplate(descriptor) + processor.setupTemplate(loadedTemplate, reporter) + return reporter + } +} diff --git a/src/test/kotlin/creator/StringCreatorPropertyTest.kt b/src/test/kotlin/creator/StringCreatorPropertyTest.kt new file mode 100644 index 000000000..d30e51953 --- /dev/null +++ b/src/test/kotlin/creator/StringCreatorPropertyTest.kt @@ -0,0 +1,335 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationItem +import com.demonwav.mcdev.util.firstOfType +import kotlin.collections.singleOrNull +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("String Creator Property Tests") +class StringCreatorPropertyTest : CreatorTemplateProcessorTestBase() { + + @Test + @DisplayName("Invalid Validator") + fun invalidValidator() { + val reporter = makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "validator": "[invalid" + } + ] + } + """.trimIndent() + ) + + assertTrue(reporter.hasErrors) + assertEquals( + "Invalid validator regex: '[invalid': Unclosed character class near index 7\n[invalid\n ^", + reporter.items["STRING"]?.singleOrNull()?.message?.replace("\r\n", "\n") + ) + } + + @Test + @DisplayName("Replace Derivation") + fun replaceDerivation() { + makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["PROJECT_NAME"], + "method": "replace", + "parameters": { + "regex": "[^a-z0-9-_]+", + "replacement": "_", + "maxLength": 32, + "lowercase": true + } + } + } + ] + } + """.trimIndent() + ) + + val projectNameProperty = processor.context.property("PROJECT_NAME") + val stringProperty = processor.context.property("STRING") + + projectNameProperty.graphProperty.set("Sanitize This") + assertEquals("sanitize_this", stringProperty.get()) + + projectNameProperty.graphProperty.set("This string will get truncated at some point") + assertEquals("this_string_will_get_truncated_a", stringProperty.get()) + } + + @Test + @DisplayName("Replace Derivation Missing Parameters") + fun replaceDerivationMissingParameters() { + val reporter = makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["PROJECT_NAME"], + "method": "replace" + } + } + ] + } + """.trimIndent() + ) + + assertEquals("Missing parameters", reporter.items["STRING"]?.singleOrNull()?.message) + } + + @Test + @DisplayName("Replace Derivation Missing Parent Value") + fun replaceDerivationMissingParentValue() { + val reporter = makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": [], + "method": "replace", + "parameters": {} + } + } + ] + } + """.trimIndent() + ) + + assertEquals("Missing parent value", reporter.items["STRING"]?.singleOrNull()?.message) + } + + @Test + @DisplayName("Replace Derivation More Than One Parent Defined") + fun replaceDerivationMoreThanOneParentDefined() { + val reporter = makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["PROJECT_NAME", "PROJECT_NAME"], + "method": "replace", + "parameters": {} + } + } + ] + } + """.trimIndent() + ) + + assertEquals( + "More than one parent defined", + reporter.items["STRING"]?.firstOfType()?.message + ) + } + + @Test + @DisplayName("Replace Derivation Parent Property Must Produce A String Value") + fun replaceDerivationParentPropertyMustProduceAStringValue() { + val reporter = makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "BOOL", + "type": "boolean" + }, + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["BOOL"], + "method": "replace", + "parameters": {} + } + } + ] + } + """.trimIndent() + ) + + assertEquals("Parent property must produce a string value", reporter.items["STRING"]?.singleOrNull()?.message) + } + + @Test + @DisplayName("Replace Derivation Missing Regex Parameter") + fun replaceDerivationMissingRegexParameter() { + val reporter = makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["PROJECT_NAME"], + "method": "replace", + "parameters": { + "replacement": "_" + } + } + } + ] + } + """.trimIndent() + ) + + assertEquals("Missing 'regex' string parameter", reporter.items["STRING"]?.singleOrNull()?.message) + } + + @Test + @DisplayName("Replace Derivation Missing Replacement Parameter") + fun replaceDerivationMissingReplacementParameter() { + val reporter = makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["PROJECT_NAME"], + "method": "replace", + "parameters": { + "regex": "[^a-z0-9-_]+" + } + } + } + ] + } + """.trimIndent() + ) + + assertEquals("Missing 'replacement' string parameter", reporter.items["STRING"]?.singleOrNull()?.message) + } + + @Test + @DisplayName("Select Derivation") + fun selectDerivation() { + makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["PROJECT_NAME"], + "select": [ + { "condition": "${'$'}PROJECT_NAME == 'Name 1'", "value": "Value 1" }, + { "condition": "${'$'}PROJECT_NAME == 'Name 2'", "value": "Value 2" }, + { "condition": "${'$'}PROJECT_NAME == 'Name 3'", "value": "Value 3" } + ], + "default": "Default value" + } + } + ] + } + """.trimIndent() + ) + + val projectNameProperty = processor.context.property("PROJECT_NAME") + val stringProperty = processor.context.property("STRING") + + projectNameProperty.graphProperty.set("Name 1") + assertEquals("Value 1", stringProperty.get()) + + projectNameProperty.graphProperty.set("Name 2") + assertEquals("Value 2", stringProperty.get()) + + projectNameProperty.graphProperty.set("Name 3") + assertEquals("Value 3", stringProperty.get()) + + projectNameProperty.graphProperty.set("Name 4") + assertEquals("Default value", stringProperty.get()) + } + + @Test + @DisplayName("Select Derivation With Modification") + fun selectDerivationWithModification() { + makeTemplate( + """ + { + "version": ${TemplateDescriptor.FORMAT_VERSION}, + "properties": [ + { + "name": "STRING", + "type": "string", + "derives": { + "parents": ["PROJECT_NAME"], + "select": [ + { "condition": "${'$'}PROJECT_NAME == 'Name 1'", "value": "Value 1" }, + { "condition": "${'$'}PROJECT_NAME == 'Name 2'", "value": "Value 2" } + ], + "default": "Default value", + "whenModified": true + } + } + ] + } + """.trimIndent() + ) + + val projectNameProperty = processor.context.property("PROJECT_NAME") + val stringProperty = processor.context.property("STRING") + + projectNameProperty.graphProperty.set("Name 1") + assertEquals("Value 1", stringProperty.get()) + + stringProperty.graphProperty.set("Custom value") + projectNameProperty.graphProperty.set("Name 2") + assertEquals("Custom value", stringProperty.get()) + } +} diff --git a/templates b/templates index c8cf7b83d..66979e787 160000 --- a/templates +++ b/templates @@ -1 +1 @@ -Subproject commit c8cf7b83d9f15903c40e603725318de5bcba85f8 +Subproject commit 66979e78735419e08a6ba5dfbc3031e45da83b5b