diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AbstractEnumOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AbstractEnumOptionValue.kt deleted file mode 100644 index 55214b3..0000000 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AbstractEnumOptionValue.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues - -import com.intellij.openapi.project.Project - -abstract class AbstractEnumOptionValue(private val validOptions: Set, override val validatorName: String): OptionValueInformation { - - override fun getAutoCompleteOptions(project: Project): Set { - return validOptions - } - - - override fun getErrorMessage(value: String): String? { - return if (!validOptions.contains(value)) { - "The value supplied $value does not match one of the expected values: $validOptions" - } else { - null - } - } - -} 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 index ca80993..92e7991 100644 --- 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 @@ -6,17 +6,14 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram 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. + * Routing through the grammar engine gives each enum precise error highlighting and + * replace-with-valid-choice quick-fixes. The curated choice list is still returned for + * autocomplete, so completion does not regress (a bare [grammar.GrammarOptionValue] would + * suggest nothing). */ abstract class AbstractGrammarEnumOptionValue( private val validOptions: Set, 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 270bbf8..000f705 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 @@ -5,7 +5,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator -class EmergencyActionOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME){ +class EmergencyActionOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDATOR_NAME){ companion object { private val validOptions : Set = ImmutableSet.of("none", "reboot", "reboot-force", "reboot-immediate", "poweroff", "poweroff-force", "poweroff-immediate", "exit", "exit-force") @@ -21,7 +21,7 @@ class KillModeOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDAT } } -class ManagedOOMModeOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME){ +class ManagedOOMModeOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDATOR_NAME){ companion object { private val validOptions : Set = ImmutableSet.of("auto", "kill") @@ -29,7 +29,7 @@ class ManagedOOMModeOptionValue : AbstractEnumOptionValue(validOptions, VALIDATO } } -class RestartOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME) { +class RestartOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDATOR_NAME) { companion object { private val validOptions: Set = ImmutableSet.of("no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always") @@ -37,7 +37,7 @@ class RestartOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME) } } -class ServiceTypeOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME) { +class ServiceTypeOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDATOR_NAME) { companion object { private val validOptions: Set = ImmutableSet.of("simple", "forking", "oneshot", "dbus", "notify", "notify-reload", "idle", "exec") @@ -45,14 +45,14 @@ class ServiceTypeOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_N } } -class ProtectProcOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME) { +class ProtectProcOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDATOR_NAME) { companion object { private val validOptions: Set = ImmutableSet.of("noaccess", "invisible", "ptraceable", "default") const val VALIDATOR_NAME = "config_parse_protect_proc" } } -class ProcSubsetOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NAME) { +class ProcSubsetOptionValue : AbstractGrammarEnumOptionValue(validOptions, VALIDATOR_NAME) { companion object { private val validOptions: Set = ImmutableSet.of("all", "pid") const val VALIDATOR_NAME = "config_parse_proc_subset" @@ -60,7 +60,7 @@ class ProcSubsetOptionValue : AbstractEnumOptionValue(validOptions, VALIDATOR_NA } } -class KeyRingModeOptionValue: AbstractEnumOptionValue(ImmutableSet.of("inherit", "private", "shared"), VALIDATOR_NAME) { +class KeyRingModeOptionValue: AbstractGrammarEnumOptionValue(ImmutableSet.of("inherit", "private", "shared"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_exec_keyring_mode" } @@ -68,96 +68,97 @@ class KeyRingModeOptionValue: AbstractEnumOptionValue(ImmutableSet.of("inherit", -class PersonalityOptionValue : AbstractEnumOptionValue(ImmutableSet.of("arm64","arm64-be","arm","arm-be","alpha","arc","arc-be","cris","x86-64","x86","ia64","loongarch64","m68k","mips64-le","mips64","mips-le","mips","nios2","parisc64","parisc","ppc64-le","ppc64","ppc","ppc-le","riscv32","riscv64","s390x","s390","sh64","sh","sparc64","sparc","tilegx"), VALIDATOR_NAME) { +class PersonalityOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("arm64","arm64-be","arm","arm-be","alpha","arc","arc-be","cris","x86-64","x86","ia64","loongarch64","m68k","mips64-le","mips64","mips-le","mips","nios2","parisc64","parisc","ppc64-le","ppc64","ppc","ppc-le","riscv32","riscv64","s390x","s390","sh64","sh","sparc64","sparc","tilegx"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_personality" } } -class CpuSchedulingPolicyOptionValue : AbstractEnumOptionValue(ImmutableSet.of("other", "batch", "idle", "fifo", "rr"), VALIDATOR_NAME) { +class CpuSchedulingPolicyOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("other", "batch", "idle", "fifo", "rr"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_exec_cpu_sched_policy" } } -class NumaPolicyOptionValue : AbstractEnumOptionValue(ImmutableSet.of("default", "bind", "interleave", "local"), VALIDATOR_NAME) { +class NumaPolicyOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("default", "bind", "interleave", "local"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_numa_policy" } } -class IOSchedulingClassOptionValue : AbstractEnumOptionValue(ImmutableSet.of("realtime", "best-effort", "idle"), VALIDATOR_NAME) { +class IOSchedulingClassOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("realtime", "best-effort", "idle"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_exec_io_class" } } -class LogLevelOptionValue : AbstractEnumOptionValue(ImmutableSet.of("emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"), VALIDATOR_NAME) { +class LogLevelOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_log_level" } } -class SysLogFacilityOptionValue : AbstractEnumOptionValue(ImmutableSet.of("kern","user", "mail", "daemon", "auth", "syslog", "lpr", "news", "uucp", "cron", "authpriv", "ftp", "local0", "local1", "local2", "local3", "local4", "local5", "local6", "local7"), VALIDATOR_NAME) { +class SysLogFacilityOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("kern","user", "mail", "daemon", "auth", "syslog", "lpr", "news", "uucp", "cron", "authpriv", "ftp", "local0", "local1", "local2", "local3", "local4", "local5", "local6", "local7"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_log_facility" } } -class UtmpModeOptionValue : AbstractEnumOptionValue(ImmutableSet.of("init", "login", "user"), VALIDATOR_NAME) { +class UtmpModeOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("init", "login", "user"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_exec_utmp_mode" } } -class DevicePolicyOptionValue : AbstractEnumOptionValue(ImmutableSet.of("auto", "closed", "strict"), VALIDATOR_NAME) { +class DevicePolicyOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("auto", "closed", "strict"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_device_policy" } } -class ManagedOOMPreferenceOptionValue : AbstractEnumOptionValue(ImmutableSet.of("none", "avoid", "omit"), VALIDATOR_NAME) { +class ManagedOOMPreferenceOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("none", "avoid", "omit"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_managed_oom_preference" } } -class ServiceExitTypeOptionValue : AbstractEnumOptionValue(ImmutableSet.of("main", "cgroup"), VALIDATOR_NAME) { +class ServiceExitTypeOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("main", "cgroup"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_service_exit_type" } } -class ServiceTimeoutFailureModeOptionValue : AbstractEnumOptionValue(ImmutableSet.of("terminate", "abort", "kill"), VALIDATOR_NAME) { +class ServiceTimeoutFailureModeOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("terminate", "abort", "kill"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_service_timeout_failure_mode" } } -class NotifyAccessOptionValue : AbstractEnumOptionValue(ImmutableSet.of("none", "main", "exec", "all"), VALIDATOR_NAME) { + +class NotifyAccessOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("none", "main", "exec", "all"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_notify_access" } } -class OOMPolicyOptionValue : AbstractEnumOptionValue(ImmutableSet.of("continue", "stop", "kill"), VALIDATOR_NAME) { +class OOMPolicyOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("continue", "stop", "kill"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_oom_policy" } } -class SocketTimestampingOptionValue : AbstractEnumOptionValue(ImmutableSet.of("off", "us", "usec", "µs", "ns", "nsec"), VALIDATOR_NAME) { +class SocketTimestampingOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("off", "us", "usec", "µs", "ns", "nsec"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_socket_timestamping" } } -class SocketProtocolOptionValue : AbstractEnumOptionValue(ImmutableSet.of("udplite", "sctp"), VALIDATOR_NAME) { +class SocketProtocolOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("udplite", "sctp"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_socket_protocol" } } -class SocketBindOptionValue : AbstractEnumOptionValue(ImmutableSet.of("default", "both", "ipv6-only"), VALIDATOR_NAME) { +class SocketBindOptionValue : AbstractGrammarEnumOptionValue(ImmutableSet.of("default", "both", "ipv6-only"), VALIDATOR_NAME) { companion object { const val VALIDATOR_NAME = "config_parse_socket_bind" 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 e075829..8cef88a 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 @@ -246,6 +246,27 @@ class InvalidValueInspectionForManagedOOMModeOptionsTest : AbstractUnitFileTest( // Verification assertSize(1, highlights) } + + fun testValueStartingWithValidChoiceButWithTrailingCharactersIsFlagged() { + // Fixture Setup + // 'autox' starts with the valid choice 'auto' but has a trailing char, so it exercises the + // syntactic format-error branch (not the semantic-choice branch); this pins it is still flagged. + // language="unit file (systemd)" + val file = """ + [Swap] + ManagedOOMSwap=autox + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.swap", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + assertStringContains("does not match the expected format", highlights[0]!!.description) + } } class InvalidValueInspectionForKillModeOptionValue : AbstractUnitFileTest() { @@ -633,6 +654,27 @@ class InvalidValueInspectionForPersonalityOptionValue : AbstractUnitFileTest() { // Verification assertSize(1, highlights) } + + fun testInvalidValueOffersReplacementQuickFixForCollidingArchitectures() { + // Fixture Setup + // Personality has the heaviest prefix-collision set (arm/arm64/arm64-be, mips/mips64/..., etc.); + // this pins that an invalid value still offers a well-formed replacement to a full-length choice. + // language="unit file (systemd)" + val file = """ + [Service] + Personality=amd64 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + assertContainsQuickfix(highlights[0]!!, "Replace 'amd64' with 'arm64'") + } } class InvalidValueInspectionForCpuSchedulingModeOptionValue : AbstractUnitFileTest() { @@ -1130,6 +1172,27 @@ class InvalidValueInspectionForSocketTimestampingOptionValue : AbstractUnitFileT // Verification assertSize(1, highlights) } + + fun testInvalidValueOffersReplacementQuickFixForUnicodeMicrosecondChoice() { + // Fixture Setup + // The 'µs' (U+00B5) spelling is non-ASCII, so it cannot appear in the ASCII syntactic regex and + // is reachable only via the exact-startsWith path; this pins that it survives into the fix list. + // language="unit file (systemd)" + val file = """ + [Socket] + Timestamping=ms + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + assertContainsQuickfix(highlights[0]!!, "Replace 'ms' with 'µs'") + } } class InvalidValueInspectionForSocketProtocolOptionValue : AbstractUnitFileTest() {