Skip to content
Open
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,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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
)
}
}
Original file line number Diff line number Diff line change
@@ -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<DeprecatedToken> {
// Require a fully-valid parse: don't pile a deprecation note on top of an otherwise invalid value.
val parse = parse(value, 0).filterIsInstance<Parse>()
.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) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin
choices.sortBy { -it.length }
}

private var deprecations: Map<String, String> = emptyMap()

/** Mark some choices as valid-but-deprecated (choice -> reason). Returns this for chaining. */
fun deprecating(deprecations: Map<String, String>): FlexibleLiteralChoiceTerminal {
this.deprecations = deprecations
return this
}

override fun deprecationFor(token: String): String? = deprecations[token]

val syntaticMatch: Regex

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.PidFileOptionWarning"/>
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.GrammarEngineKeyAnnotator"/>
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.GrammarValueColorAnnotator"/>
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.DeprecatedGrammarValueAnnotator"/>
<localInspection implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.inspections.UnknownKeyInSectionInspection"
groupPath="Unit files (systemd)" language="Unit File (systemd)"
shortName="UnknownKeyInSection" displayName="Unknown option in section"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.sjrx.intellij.plugins.systemdunitfiles.annotators

import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.lang.annotation.HighlightSeverity
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
import org.junit.Test

/** End-to-end: a kernel-removed address family gets a weak warning (behind the flag). */
class DeprecatedGrammarValueAnnotatorTest : AbstractUnitFileTest() {

override fun tearDown() {
try {
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = false
} finally {
super.tearDown()
}
}

private fun enableNewEngine() {
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = true
}

private fun weakWarned(highlights: List<HighlightInfo>, 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"))
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
Loading