diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AbstractGrammarEnumOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AbstractGrammarEnumOptionValue.kt new file mode 100644 index 0000000..ca80993 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AbstractGrammarEnumOptionValue.kt @@ -0,0 +1,40 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues + +import com.intellij.openapi.project.Project +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/** + * Enum-style validator backed by the parser-combinator engine instead of a plain set-membership check. + * + * Same "just give me the valid choices" API as [AbstractEnumOptionValue], but by routing through the + * grammar engine each migrated enum gets precise error highlighting and replace-with-valid-choice + * quick-fixes for free. The curated choice list is still returned for autocomplete, so completion does + * not regress (a bare [grammar.GrammarOptionValue] would suggest nothing). + * + * Migration plan: move validators in [EnumOptionValues] onto this base one merge request at a time. + * This class is intentionally dormant until the first subclass exists — nothing constructs it yet, so + * [buildGrammar] is not invoked and the existing suite is unaffected. + */ +abstract class AbstractGrammarEnumOptionValue( + private val validOptions: Set, + validatorName: String, +) : SimpleGrammarOptionValues(validatorName, buildGrammar(validOptions)) { + + override fun getAutoCompleteOptions(project: Project): Set = validOptions + + override fun invalidValueMessage(key: String, badValue: String): String { + return "$key's value '$badValue' does not match one of the expected values: $validOptions" + } + + companion object { + /** + * Turn the set of valid enum spellings into the grammar the engine validates against. + */ + private fun buildGrammar(validOptions: Set): Combinator { + return SequenceCombinator(FlexibleLiteralChoiceTerminal(choices = validOptions.toTypedArray(), ignoreCase = false), EOF()) + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/EnumOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/EnumOptionValues.kt index 71eb8fc..270bbf8 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/EnumOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/EnumOptionValues.kt @@ -13,7 +13,7 @@ class EmergencyActionOptionValue : AbstractEnumOptionValue(validOptions, VALIDAT } } -class KillModeOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME) { +class KillModeOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDATOR_NAME) { companion object { private val validOptions: Set = ImmutableSet.of("control-group", "process", "mixed", "none") diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt index a8567bf..1e474d2 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt @@ -28,6 +28,15 @@ open class GrammarOptionValue( throw IllegalStateException("This should not be called") } + /** + * Message for a value that is well-formed but not actually valid (the "semantic" error, e.g. a + * value of the right shape that is not one of the allowed choices). Both matching engines call + * this, so an override applies regardless of the parse-engine flag. Subclasses that know their + * allowed values (e.g. enum validators) override it to name them; the default stays generic. + */ + open fun invalidValueMessage(key: String, badValue: String): String = + "$key's value is correctly formatted but seems invalid." + /** * Generates problem descriptors based on the value. * @@ -102,9 +111,9 @@ open class GrammarOptionValue( } } - holder.registerProblem(property.valueNode.psi, "${property.key}'s value is correctly formatted but seems invalid.", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, tr, *quickFixes.toTypedArray()) + holder.registerProblem(property.valueNode.psi, invalidValueMessage(property.key, problemToken), ProblemHighlightType.GENERIC_ERROR_OR_WARNING, tr, *quickFixes.toTypedArray()) } else { - holder.registerProblem(property.valueNode.psi, "${property.key}'s value is correctly formatted but seems invalid.", ProblemHighlightType.GENERIC_ERROR_OR_WARNING) + holder.registerProblem(property.valueNode.psi, invalidValueMessage(property.key, value), ProblemHighlightType.GENERIC_ERROR_OR_WARNING) } @@ -166,7 +175,7 @@ open class GrammarOptionValue( quickFixes.add(ReplaceInvalidLiteralChoiceQuickFix(bad.start, bad.text, choice)) } - holder.registerProblem(property.valueNode.psi, "${property.key}'s value is correctly formatted but seems invalid.", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, tr, *quickFixes.toTypedArray()) + holder.registerProblem(property.valueNode.psi, invalidValueMessage(property.key, bad.text), ProblemHighlightType.GENERIC_ERROR_OR_WARNING, tr, *quickFixes.toTypedArray()) } } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueInspectionForEnumOptionValueTests.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueInspectionForEnumOptionValueTests.kt index f9e22bc..e075829 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueInspectionForEnumOptionValueTests.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueInspectionForEnumOptionValueTests.kt @@ -303,6 +303,24 @@ class InvalidValueInspectionForKillModeOptionValue : AbstractUnitFileTest() { TestCase.assertNotNull(highlightElement) TestCase.assertEquals("sigkill", highlightElement!!.text) } + + fun testKillModeInvalidValueOffersReplacementQuickFix() { + // Fixture Setup + val file = """ + [Service] + KillMode=sigkill + + """.trimIndent() + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + + // Exercise SUT + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + assertContainsQuickfix(highlights[0]!!, "Replace 'sigkill' with 'process'") + } } class InvalidValueInspectionForRestartOptionValueTest : AbstractUnitFileTest() {