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
@@ -1,62 +1,23 @@
package net.sjrx.intellij.plugins.systemdunitfiles.inspections

import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.PsiTreeUtil
import net.sjrx.intellij.plugins.systemdunitfiles.UnitFileLanguage
import com.intellij.codeInspection.LocalQuickFix
import net.sjrx.intellij.plugins.systemdunitfiles.intentions.CanonicalizeIpv6QuickFix
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFile
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionGroups
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileVisitor
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.canonicalizeIpv6
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SemanticTag
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.labeledRegions
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.canonicalizeIpv6
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings

/**
* Suggests rewriting an IPv6 address to its RFC 5952 canonical form (#363), behind the experimental
* flag. It walks the grammar's labeled value spans and, for those the grammar tagged
* [SemanticTag.IPV6], offers a quick-fix when the address isn't already canonical. Keying off the tag
* (rather than re-sniffing every literal span) means it only ever touches spans the grammar declared
* to be IPv6 addresses.
* Suggests rewriting an IPv6 address (a span tagged [SemanticTag.IPV6]) to its RFC 5952 canonical form
* (#363), behind the grammar flag. Addresses [canonicalizeIpv6] can't handle (e.g. an IPv4 tail) are
* left alone.
*/
class Ipv6CanonicalFormInspection : LocalInspectionTool() {
class Ipv6CanonicalFormInspection : TaggedSpanRewriteInspection() {

override val tag = SemanticTag.IPV6

override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
val file = holder.file
if (file !is UnitFile || !file.language.isKindOf(UnitFileLanguage.INSTANCE)) return PsiElementVisitor.EMPTY_VISITOR
if (!ExperimentalSettings.getInstance(file.project).state.useGrammarParseEngine) return PsiElementVisitor.EMPTY_VISITOR
return MyVisitor(holder)
}
override fun rewriter(settings: ExperimentalSettings.State): ((String) -> String?) = ::canonicalizeIpv6

private class MyVisitor(private val holder: ProblemsHolder) : UnitFileVisitor() {
override fun visitPropertyType(property: UnitFilePropertyType) {
val section = PsiTreeUtil.getParentOfType(property, UnitFileSectionGroups::class.java) ?: return
val value = property.valueText ?: return
val validator = SemanticDataRepository.instance
.getOptionValidator(section.containingFile.fileClass(), section.sectionName, property.key)
if (validator !is GrammarOptionValue) return
override fun message(replacement: String) = "IPv6 address is not in canonical form (RFC 5952); use '$replacement'"

for (region in validator.combinator.labeledRegions(value)) {
if (region.tag != SemanticTag.IPV6) continue // act only on spans the grammar declared IPv6
val text = value.substring(region.start, region.end)
val canonical = canonicalizeIpv6(text) ?: continue // e.g. an IPv4-tail form, out of scope
if (canonical == text) continue
holder.registerProblem(
property.valueNode.psi,
"IPv6 address is not in canonical form (RFC 5952); use '$canonical'",
ProblemHighlightType.WEAK_WARNING,
TextRange(region.start, region.end),
CanonicalizeIpv6QuickFix(region.start, text, canonical),
)
}
}
}
override fun quickFix(original: String, replacement: String): LocalQuickFix = CanonicalizeIpv6QuickFix(original, replacement)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.sjrx.intellij.plugins.systemdunitfiles.inspections

import com.intellij.codeInspection.LocalQuickFix
import net.sjrx.intellij.plugins.systemdunitfiles.intentions.NormalizeMacFormatQuickFix
import net.sjrx.intellij.plugins.systemdunitfiles.intentions.SetPreferredMacFormatQuickFix
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SemanticTag
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.formatMac
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.macFormatOf
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
import net.sjrx.intellij.plugins.systemdunitfiles.settings.PreferredMacFormat

/**
* Suggests rewriting a 6-byte MAC address (a span tagged [SemanticTag.MAC_ADDRESS]) to the user's
* [PreferredMacFormat], behind the grammar flag. Off when the preference is [PreferredMacFormat.LEAVE_ALONE].
*/
class MacAddressFormatInspection : TaggedSpanRewriteInspection() {

override val tag = SemanticTag.MAC_ADDRESS

override fun rewriter(settings: ExperimentalSettings.State): ((String) -> String?)? {
val format = settings.preferredMacFormat
if (format == PreferredMacFormat.LEAVE_ALONE) return null
return { formatMac(it, format) }
}

override fun message(replacement: String) = "MAC address is not in preferred format, use: '$replacement'"

override fun quickFix(original: String, replacement: String): LocalQuickFix = NormalizeMacFormatQuickFix(original, replacement)

// If the address is already written in one of the other formats, offer to make that the preference instead.
override fun extraQuickFixes(original: String): List<LocalQuickFix> =
listOfNotNull(macFormatOf(original)?.let(::SetPreferredMacFormatQuickFix))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package net.sjrx.intellij.plugins.systemdunitfiles.inspections

import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.PsiTreeUtil
import net.sjrx.intellij.plugins.systemdunitfiles.UnitFileLanguage
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFile
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionGroups
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileVisitor
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.SemanticTag
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.labeledRegions
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings

/**
* Base for inspections that suggest rewriting spans the grammar tagged with [tag] (behind the grammar
* engine flag): for each such span, if [rewrite] gives a different spelling, report a weak warning with
* a quick fix. Keying off the tag means only spans the grammar *declared* are touched, never a literal
* that merely looks similar.
*
* Region offsets are in the property's valueText (lines joined and trimmed), so they're mapped back
* onto the raw value element with [valueTextOffsets] before reporting; values whose mapping fails, or
* whose mapped text doesn't match, are skipped rather than highlighted in the wrong place.
*/
abstract class TaggedSpanRewriteInspection : LocalInspectionTool() {

protected abstract val tag: SemanticTag

/** The rewrite to apply under the current [settings], or null to report nothing at all. */
protected abstract fun rewriter(settings: ExperimentalSettings.State): ((String) -> String?)?

protected abstract fun message(replacement: String): String

protected abstract fun quickFix(original: String, replacement: String): LocalQuickFix

/** Further fixes offered next to [quickFix] that don't rewrite the span (e.g. changing the preference). */
protected open fun extraQuickFixes(original: String): List<LocalQuickFix> = emptyList()

override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
val file = holder.file
if (file !is UnitFile || !file.language.isKindOf(UnitFileLanguage.INSTANCE)) return PsiElementVisitor.EMPTY_VISITOR
val settings = ExperimentalSettings.getInstance(file.project).state
if (!settings.useGrammarParseEngine) return PsiElementVisitor.EMPTY_VISITOR
val rewrite = rewriter(settings) ?: return PsiElementVisitor.EMPTY_VISITOR

return object : UnitFileVisitor() {
override fun visitPropertyType(property: UnitFilePropertyType) {
val section = PsiTreeUtil.getParentOfType(property, UnitFileSectionGroups::class.java) ?: return
val value = property.valueText ?: return
val validator = SemanticDataRepository.instance
.getOptionValidator(section.containingFile.fileClass(), section.sectionName, property.key)
if (validator !is GrammarOptionValue) return

val regions = validator.combinator.labeledRegions(value).filter { it.tag == tag && it.start < it.end }
if (regions.isEmpty()) return
val valueElement = property.valueNode.psi
val rawText = valueElement.text
val offsets = valueTextOffsets(valueElement, value) ?: return

for (region in regions) {
val text = value.substring(region.start, region.end)
val replacement = rewrite(text) ?: continue
if (replacement == text) continue
val range = TextRange(offsets[region.start], offsets[region.end - 1] + 1)
if (range.substring(rawText) != text) continue
holder.registerProblem(valueElement, message(replacement), ProblemHighlightType.WEAK_WARNING, range, quickFix(text, replacement), *extraQuickFixes(text).toTypedArray())
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class ValueWarningInspection : LocalInspectionTool() {
* UnitFileValueType.getValue(): each value line is trimmed (String.trim(), i.e. chars <= ' '), a trailing continuation
* backslash becomes a space, and the joined result is trimmed again. Null if the reconstruction does not match [value].
*/
private fun valueTextOffsets(valueElement: PsiElement, value: String): IntArray? {
internal fun valueTextOffsets(valueElement: PsiElement, value: String): IntArray? {
val text = StringBuilder()
val offsets = mutableListOf<Int>()
var child = valueElement.firstChild
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
package net.sjrx.intellij.plugins.systemdunitfiles.intentions

import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.intellij.psi.util.PsiTreeUtil
import net.sjrx.intellij.plugins.systemdunitfiles.psi.impl.UnitFilePropertyImpl

/**
* Replaces the IPv6 address at [offset] (within the option value) with its RFC 5952 [canonical] form.
* Replaces the IPv6 address [original] with its RFC 5952 [canonical] form.
*/
class CanonicalizeIpv6QuickFix(private val offset: Int, private val original: String, private val canonical: String) : LocalQuickFix {

override fun getName(): String = "Convert to canonical IPv6 '$canonical'"

override fun getFamilyName(): String = "Convert to canonical IPv6 (RFC 5952)"

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val fullPropertyValue = descriptor.psiElement.text
val newText = fullPropertyValue.substring(0, offset) + canonical + fullPropertyValue.substring(offset + original.length)
val property = PsiTreeUtil.getParentOfType(descriptor.psiElement, UnitFilePropertyImpl::class.java) ?: return
val newElement = UnitElementFactory.createProperty(project, property.key, newText)
property.replace(newElement)
}
}
class CanonicalizeIpv6QuickFix(original: String, canonical: String) : ReplaceValueSpanQuickFix(
original,
canonical,
name = "Convert to canonical IPv6 '$canonical'",
family = "Convert to canonical IPv6 (RFC 5952)",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.sjrx.intellij.plugins.systemdunitfiles.intentions

/**
* Rewrites a MAC address from its current spelling [original] to the user's preferred [replacement],
* e.g. `00-1A-2B-3C-4D-5E` -> `00:1a:2b:3c:4d:5e`.
*/
class NormalizeMacFormatQuickFix(original: String, replacement: String) : ReplaceValueSpanQuickFix(
original,
replacement,
name = "Convert MAC address to '$replacement'",
family = "Convert MAC address to preferred format",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package net.sjrx.intellij.plugins.systemdunitfiles.intentions

import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager

/**
* Replaces one span of an option value, [original], with [replacement], editing the document in place
* so the rest of the value (other entries, line continuations, indentation) is left untouched.
*
* The span is the descriptor's range within its element. A value can carry several of these fixes (e.g.
* a list of MAC addresses), and applying one shifts the text after it, so the recorded range can be
* stale by the time a later fix runs ("Fix all"). The fix therefore only edits when the range still
* holds [original]; failing that, it falls back to a single whole-token occurrence of [original]. If
* neither holds it does nothing rather than corrupt the value.
*/
open class ReplaceValueSpanQuickFix(
private val original: String,
private val replacement: String,
private val name: String,
private val family: String,
) : LocalQuickFix {

override fun getName(): String = name

override fun getFamilyName(): String = family

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement ?: return
if (!element.isValid) return
val start = locate(element.text, descriptor.textRangeInElement) ?: return
val documentManager = PsiDocumentManager.getInstance(project)
val document = documentManager.getDocument(element.containingFile) ?: return
val absoluteStart = element.textRange.startOffset + start
document.replaceString(absoluteStart, absoluteStart + original.length, replacement)
documentManager.commitDocument(document)
}

private fun locate(text: String, range: TextRange?): Int? {
if (range != null && range.endOffset <= text.length && range.substring(text) == original) {
return range.startOffset
}
val matches = generateSequence(text.indexOf(original).takeIf { it >= 0 }) { previous ->
text.indexOf(original, previous + 1).takeIf { it >= 0 }
}.filter { isWholeToken(text, it) }.take(2).toList()
return matches.singleOrNull()
}

private fun isWholeToken(text: String, start: Int): Boolean {
val end = start + original.length
return (start == 0 || text[start - 1].isWhitespace()) && (end == text.length || text[end].isWhitespace())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.sjrx.intellij.plugins.systemdunitfiles.intentions

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.project.Project
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
import net.sjrx.intellij.plugins.systemdunitfiles.settings.PreferredMacFormat

/**
* Keeps the MAC address as written and makes its [format] the preferred one instead, so the file stops
* being flagged. Changes the project setting only; the file is not edited.
*/
class SetPreferredMacFormatQuickFix(private val format: PreferredMacFormat) : LocalQuickFix {

override fun getName(): String = "Set preferred MAC address format to ${format.displayName}"

override fun getFamilyName(): String = "Set preferred MAC address format"

override fun startInWriteAction(): Boolean = false

// The default preview would run applyFix, which changes the real setting.
override fun generatePreview(project: Project, previewDescriptor: ProblemDescriptor): IntentionPreviewInfo =
IntentionPreviewInfo.Html("Changes <b>Settings | Tools | systemd Unit Files | Preferred MAC address format</b> to ${format.displayName}.")

override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
ExperimentalSettings.getInstance(project).state.preferredMacFormat = format
DaemonCodeAnalyzer.getInstance(project).restart()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,6 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/ether-addr-util.c#L196-L280 parse_hw_addr_full (via parse_ether_addr)
*/

/**
* The six bytes of a [MAC_ADDRESS] spelling: colon- or hyphen-separated 1-byte fields, or three
* dot-separated 2-byte fields (`0223.4567.89ab`, where a short field such as `123` is 0x0123).
*/
private fun macAddressBytes(text: String): IntArray? {
val bytes = when {
'.' in text -> text.split('.').flatMap { word ->
val v = word.toIntOrNull(16) ?: return null
listOf(v shr 8, v and 0xFF)
}
else -> text.split(':', '-').map { it.toIntOrNull(16) ?: return null }
}
return if (bytes.size == 6) bytes.toIntArray() else null
}

/**
* Validator for [Bond] AdActorSystem= (.netdev).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MAC_ADDRESS
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SECUREON_PASSWORD
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SAFE_ABSOLUTE_PATH
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator

Expand All @@ -29,7 +29,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram
class ConfigParseWolPasswordOptionValue : SimpleGrammarOptionValues(
"config_parse_wol_password",
AlternativeCombinator(
SequenceCombinator(MAC_ADDRESS, EOF()),
SequenceCombinator(SECUREON_PASSWORD, EOF()),
SequenceCombinator(SAFE_ABSOLUTE_PATH, EOF()),
)
)
Loading
Loading