Skip to content
Merged
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
@@ -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<String>,
validatorName: String,
) : SimpleGrammarOptionValues(validatorName, buildGrammar(validOptions)) {

override fun getAutoCompleteOptions(project: Project): Set<String> = 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<String>): Combinator {
return SequenceCombinator(FlexibleLiteralChoiceTerminal(choices = validOptions.toTypedArray(), ignoreCase = false), EOF())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = ImmutableSet.of("control-group", "process", "mixed", "none")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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)
}


Expand Down Expand Up @@ -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())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading