Skip to content

Commit 17f263b

Browse files
Steve Ramageclaude
andcommitted
feat: warn on valid-but-deprecated grammar values behind the experimental flag
A value can be perfectly valid yet use an obsolete token — e.g. an address family the kernel removed but af_from_name still resolves. Add a reusable deprecation layer: - TerminalCombinator.deprecationFor(token): String? = null — additive hook. - FlexibleLiteralChoiceTerminal.deprecating(map) marks choices obsolete. - Combinator.deprecatedTokens(value) reports them from the first fully-VALID parse, so a deprecation note never stacks on an InvalidValue error. - DeprecatedGrammarValueAnnotator renders them as WEAK_WARNINGs, gated behind ExperimentalSettings.useGrammarParseEngine (off by default, like the other grammar-engine features). First user: RestrictAddressFamilies= warns on AF_DECnet / AF_IRDA / AF_ECONET / AF_WANPIPE (kernel-removed, reasons per address_families(7)). Re-cut of stacked PR #484 against current 242.x. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 360c662 commit 17f263b

8 files changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.annotators
2+
3+
import com.intellij.lang.annotation.AnnotationHolder
4+
import com.intellij.lang.annotation.Annotator
5+
import com.intellij.lang.annotation.HighlightSeverity
6+
import com.intellij.openapi.util.TextRange
7+
import com.intellij.psi.PsiElement
8+
import com.intellij.psi.util.PsiTreeUtil
9+
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileProperty
10+
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionType
11+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.SemanticDataRepository
12+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.fileClass
13+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue
14+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.deprecatedTokens
15+
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
16+
17+
/**
18+
* Flags valid-but-deprecated values with a weak warning (#467), behind the experimental flag.
19+
*
20+
* Generic: any grammar can mark choices deprecated (see [GrammarOptionValue]'s terminals). The first
21+
* user is RestrictAddressFamilies=, warning on kernel-removed families like AF_DECnet.
22+
*/
23+
class DeprecatedGrammarValueAnnotator : Annotator {
24+
25+
override fun annotate(element: PsiElement, holder: AnnotationHolder) {
26+
if (element !is UnitFileProperty) return
27+
if (!ExperimentalSettings.getInstance(element.project).state.useGrammarParseEngine) return
28+
29+
val section = PsiTreeUtil.getParentOfType(element, UnitFileSectionType::class.java) ?: return
30+
val value = element.valueText ?: return
31+
val base = element.valueNode?.psi?.textRange?.startOffset ?: return
32+
val fileClass = element.containingFile.fileClass()
33+
val validator = SemanticDataRepository.instance.getOptionValidator(fileClass, section.sectionName, element.key)
34+
if (validator !is GrammarOptionValue) return
35+
36+
for (deprecated in validator.combinator.deprecatedTokens(value)) {
37+
holder.newAnnotation(HighlightSeverity.WEAK_WARNING, deprecated.message)
38+
.range(TextRange(base + deprecated.start, base + deprecated.end))
39+
.create()
40+
}
41+
}
42+
}

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressFamiliesOptionValue.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ class ConfigParseAddressFamiliesOptionValue : SimpleGrammarOptionValues(
9898
"AF_WANPIPE",
9999
"AF_X25",
100100
"AF_XDP"
101+
).deprecating(
102+
// Still resolved by af_from_name (the libc macro exists) but the kernel removed the
103+
// protocol, so configuring them has no effect. Reasons per address_families(7).
104+
mapOf(
105+
"AF_DECnet" to "AF_DECnet is obsolete: DECnet support was removed from the Linux kernel in 6.1.",
106+
"AF_IRDA" to "AF_IRDA is obsolete: IrDA support was removed from the Linux kernel in 4.17.",
107+
"AF_ECONET" to "AF_ECONET is obsolete: Acorn Econet support was removed from the Linux kernel in 3.5.",
108+
"AF_WANPIPE" to "AF_WANPIPE is obsolete: WANPIPE support was removed from the Linux kernel in 2.6.21.",
109+
)
101110
)
102111
}
103112
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar
2+
3+
/*
4+
* Valid-but-deprecated value detection (#467). A reusable companion to validation: a value can be
5+
* perfectly valid yet use an obsolete token (e.g. an address family the kernel removed). Terminals
6+
* declare which of their matched values are deprecated via [TerminalCombinator.deprecationFor].
7+
*/
8+
9+
/** A deprecated token at `[start, end)` in the value, with the reason to show. */
10+
data class DeprecatedToken(val start: Int, val end: Int, val message: String)
11+
12+
/**
13+
* Deprecated tokens in [value], taken from the first full parse. Empty if the value doesn't fully
14+
* parse (an invalid value is the InvalidValue inspection's job; deprecation is reported once valid).
15+
*/
16+
fun Combinator.deprecatedTokens(value: String): List<DeprecatedToken> {
17+
// Require a fully-valid parse: don't pile a deprecation note on top of an otherwise invalid value.
18+
val parse = parse(value, 0).filterIsInstance<Parse>()
19+
.firstOrNull { it.end == value.length && it.tokens.all { token -> token.valid } } ?: return emptyList()
20+
return parse.tokens.mapNotNull { token ->
21+
token.terminal.deprecationFor(token.text)?.let { DeprecatedToken(token.start, token.end, it) }
22+
}
23+
}

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin
88
choices.sortBy { -it.length }
99
}
1010

11+
private var deprecations: Map<String, String> = emptyMap()
12+
13+
/** Mark some choices as valid-but-deprecated (choice -> reason). Returns this for chaining. */
14+
fun deprecating(deprecations: Map<String, String>): FlexibleLiteralChoiceTerminal {
15+
this.deprecations = deprecations
16+
return this
17+
}
18+
19+
override fun deprecationFor(token: String): String? = deprecations[token]
20+
1121
val syntaticMatch: Regex
1222

1323
init {

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@ interface TerminalCombinator : Combinator {
44
override fun toStringIndented(indent: Int): String {
55
return toString()
66
}
7+
8+
/**
9+
* A message if [token] (a value this terminal matched) is valid but deprecated, else null.
10+
* Lets a grammar flag accepted-but-obsolete values (e.g. kernel-removed address families).
11+
*/
12+
fun deprecationFor(token: String): String? = null
713
}

src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.LineContinuationCharacterFollowedByWhitespaceAnnotator"/>
6161
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.PropertyIsNotInSectionAnnotator"/>
6262
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.PidFileOptionWarning"/>
63+
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.DeprecatedGrammarValueAnnotator"/>
6364
<localInspection implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.inspections.UnknownKeyInSectionInspection"
6465
groupPath="Unit files (systemd)" language="Unit File (systemd)"
6566
shortName="UnknownKeyInSection" displayName="Unknown option in section"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.annotators
2+
3+
import com.intellij.codeInsight.daemon.impl.HighlightInfo
4+
import com.intellij.lang.annotation.HighlightSeverity
5+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
6+
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
7+
import org.junit.Test
8+
9+
/** End-to-end: a kernel-removed address family gets a weak warning (behind the flag). */
10+
class DeprecatedGrammarValueAnnotatorTest : AbstractUnitFileTest() {
11+
12+
override fun tearDown() {
13+
try {
14+
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = false
15+
} finally {
16+
super.tearDown()
17+
}
18+
}
19+
20+
private fun enableNewEngine() {
21+
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = true
22+
}
23+
24+
private fun weakWarned(highlights: List<HighlightInfo>, text: String) =
25+
highlights.any { it.severity == HighlightSeverity.WEAK_WARNING && it.text == text }
26+
27+
@Test
28+
fun testRemovedFamilyIsWeakWarned() {
29+
enableNewEngine()
30+
setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_INET AF_DECnet")
31+
assertTrue(weakWarned(myFixture.doHighlighting(), "AF_DECnet"))
32+
}
33+
34+
@Test
35+
fun testCurrentFamilyIsNotWarned() {
36+
enableNewEngine()
37+
setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_INET")
38+
assertFalse(weakWarned(myFixture.doHighlighting(), "AF_INET"))
39+
}
40+
41+
@Test
42+
fun testNoWarningWhenFlagOff() {
43+
setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_DECnet")
44+
assertFalse(weakWarned(myFixture.doHighlighting(), "AF_DECnet"))
45+
}
46+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseAddressFamiliesOptionValue
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Assert.assertTrue
6+
import org.junit.Test
7+
8+
/** Unit tests for valid-but-deprecated value detection on the RestrictAddressFamilies grammar. */
9+
class DeprecationsTest {
10+
11+
private val grammar = ConfigParseAddressFamiliesOptionValue().combinator
12+
13+
@Test
14+
fun testRemovedFamilyIsReportedAtItsExactSpan() {
15+
val deprecated = grammar.deprecatedTokens("AF_INET AF_DECnet")
16+
assertEquals(1, deprecated.size)
17+
val it = deprecated.single()
18+
assertEquals(8, it.start) // "AF_INET " == 8 chars
19+
assertEquals(17, it.end) // + "AF_DECnet"
20+
assertTrue(it.message.contains("AF_DECnet"))
21+
assertTrue(it.message.contains("removed"))
22+
}
23+
24+
@Test
25+
fun testCurrentFamiliesAreNotReported() {
26+
assertTrue(grammar.deprecatedTokens("AF_INET AF_INET6 AF_UNIX").isEmpty())
27+
}
28+
29+
@Test
30+
fun testInvalidValueReportsNoDeprecations() {
31+
// No full parse -> nothing (the InvalidValue inspection handles the error instead).
32+
assertTrue(grammar.deprecatedTokens("AF_DECnet AF_BOGUS").isEmpty())
33+
}
34+
}

0 commit comments

Comments
 (0)