diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/DeprecatedGrammarValueAnnotator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/DeprecatedGrammarValueAnnotator.kt new file mode 100644 index 0000000..4667ad4 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/DeprecatedGrammarValueAnnotator.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.annotators + +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileProperty +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.SemanticDataRepository +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.fileClass +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.deprecatedTokens +import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings + +/** + * Flags valid-but-deprecated values with a weak warning (#467), behind the experimental flag. + * + * Generic: any grammar can mark choices deprecated (see [GrammarOptionValue]'s terminals). The first + * user is RestrictAddressFamilies=, warning on kernel-removed families like AF_DECnet. + */ +class DeprecatedGrammarValueAnnotator : Annotator { + + override fun annotate(element: PsiElement, holder: AnnotationHolder) { + if (element !is UnitFileProperty) return + if (!ExperimentalSettings.getInstance(element.project).state.useGrammarParseEngine) return + + val section = PsiTreeUtil.getParentOfType(element, UnitFileSectionType::class.java) ?: return + val value = element.valueText ?: return + val base = element.valueNode?.psi?.textRange?.startOffset ?: return + val fileClass = element.containingFile.fileClass() + val validator = SemanticDataRepository.instance.getOptionValidator(fileClass, section.sectionName, element.key) + if (validator !is GrammarOptionValue) return + + for (deprecated in validator.combinator.deprecatedTokens(value)) { + holder.newAnnotation(HighlightSeverity.WEAK_WARNING, deprecated.message) + .range(TextRange(base + deprecated.start, base + deprecated.end)) + .create() + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressFamiliesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressFamiliesOptionValue.kt index ee814d4..b0134cd 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressFamiliesOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressFamiliesOptionValue.kt @@ -98,6 +98,15 @@ class ConfigParseAddressFamiliesOptionValue : SimpleGrammarOptionValues( "AF_WANPIPE", "AF_X25", "AF_XDP" + ).deprecating( + // Still resolved by af_from_name (the libc macro exists) but the kernel removed the + // protocol, so configuring them has no effect. Reasons per address_families(7). + mapOf( + "AF_DECnet" to "AF_DECnet is obsolete: DECnet support was removed from the Linux kernel in 6.1.", + "AF_IRDA" to "AF_IRDA is obsolete: IrDA support was removed from the Linux kernel in 4.17.", + "AF_ECONET" to "AF_ECONET is obsolete: Acorn Econet support was removed from the Linux kernel in 3.5.", + "AF_WANPIPE" to "AF_WANPIPE is obsolete: WANPIPE support was removed from the Linux kernel in 2.6.21.", + ) ) } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Deprecations.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Deprecations.kt new file mode 100644 index 0000000..307eaaa --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Deprecations.kt @@ -0,0 +1,23 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/* + * Valid-but-deprecated value detection (#467). A reusable companion to validation: a value can be + * perfectly valid yet use an obsolete token (e.g. an address family the kernel removed). Terminals + * declare which of their matched values are deprecated via [TerminalCombinator.deprecationFor]. + */ + +/** A deprecated token at `[start, end)` in the value, with the reason to show. */ +data class DeprecatedToken(val start: Int, val end: Int, val message: String) + +/** + * Deprecated tokens in [value], taken from the first full parse. Empty if the value doesn't fully + * parse (an invalid value is the InvalidValue inspection's job; deprecation is reported once valid). + */ +fun Combinator.deprecatedTokens(value: String): List { + // Require a fully-valid parse: don't pile a deprecation note on top of an otherwise invalid value. + val parse = parse(value, 0).filterIsInstance() + .firstOrNull { it.end == value.length && it.tokens.all { token -> token.valid } } ?: return emptyList() + return parse.tokens.mapNotNull { token -> + token.terminal.deprecationFor(token.text)?.let { DeprecatedToken(token.start, token.end, it) } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt index a2bdcaa..b1c5e23 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt @@ -8,6 +8,16 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin choices.sortBy { -it.length } } + private var deprecations: Map = emptyMap() + + /** Mark some choices as valid-but-deprecated (choice -> reason). Returns this for chaining. */ + fun deprecating(deprecations: Map): FlexibleLiteralChoiceTerminal { + this.deprecations = deprecations + return this + } + + override fun deprecationFor(token: String): String? = deprecations[token] + val syntaticMatch: Regex init { diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt index 8d1705a..016cdee 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt @@ -4,4 +4,10 @@ interface TerminalCombinator : Combinator { override fun toStringIndented(indent: Int): String { return toString() } + + /** + * A message if [token] (a value this terminal matched) is valid but deprecated, else null. + * Lets a grammar flag accepted-but-obsolete values (e.g. kernel-removed address families). + */ + fun deprecationFor(token: String): String? = null } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 49b186b..7dd61f1 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -62,6 +62,7 @@ + , text: String) = + highlights.any { it.severity == HighlightSeverity.WEAK_WARNING && it.text == text } + + @Test + fun testRemovedFamilyIsWeakWarned() { + enableNewEngine() + setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_INET AF_DECnet") + assertTrue(weakWarned(myFixture.doHighlighting(), "AF_DECnet")) + } + + @Test + fun testCurrentFamilyIsNotWarned() { + enableNewEngine() + setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_INET") + assertFalse(weakWarned(myFixture.doHighlighting(), "AF_INET")) + } + + @Test + fun testNoWarningWhenFlagOff() { + setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_DECnet") + assertFalse(weakWarned(myFixture.doHighlighting(), "AF_DECnet")) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/DeprecationsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/DeprecationsTest.kt new file mode 100644 index 0000000..b183092 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/DeprecationsTest.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseAddressFamiliesOptionValue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Unit tests for valid-but-deprecated value detection on the RestrictAddressFamilies grammar. */ +class DeprecationsTest { + + private val grammar = ConfigParseAddressFamiliesOptionValue().combinator + + @Test + fun testRemovedFamilyIsReportedAtItsExactSpan() { + val deprecated = grammar.deprecatedTokens("AF_INET AF_DECnet") + assertEquals(1, deprecated.size) + val it = deprecated.single() + assertEquals(8, it.start) // "AF_INET " == 8 chars + assertEquals(17, it.end) // + "AF_DECnet" + assertTrue(it.message.contains("AF_DECnet")) + assertTrue(it.message.contains("removed")) + } + + @Test + fun testCurrentFamiliesAreNotReported() { + assertTrue(grammar.deprecatedTokens("AF_INET AF_INET6 AF_UNIX").isEmpty()) + } + + @Test + fun testInvalidValueReportsNoDeprecations() { + // No full parse -> nothing (the InvalidValue inspection handles the error instead). + assertTrue(grammar.deprecatedTokens("AF_DECnet AF_BOGUS").isEmpty()) + } +}