diff --git a/src/main/gen/net/sjrx/intellij/plugins/systemdunitfiles/generated/UnitFileElementTypeHolder.java b/src/main/gen/net/sjrx/intellij/plugins/systemdunitfiles/generated/UnitFileElementTypeHolder.java index 0bc0e12f..aef53458 100644 --- a/src/main/gen/net/sjrx/intellij/plugins/systemdunitfiles/generated/UnitFileElementTypeHolder.java +++ b/src/main/gen/net/sjrx/intellij/plugins/systemdunitfiles/generated/UnitFileElementTypeHolder.java @@ -20,6 +20,7 @@ public interface UnitFileElementTypeHolder { IElementType KEY = new UnitFileTokenType("KEY"); IElementType SECTION = new UnitFileTokenType("SECTION"); IElementType SEPARATOR = new UnitFileTokenType("SEPARATOR"); + class Factory { public static PsiElement createElement(ASTNode node) { diff --git a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java index e7eb364c..02b26b2b 100644 --- a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java +++ b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java @@ -22,8 +22,18 @@ public class UnitFileHighlighter extends SyntaxHighlighterBase { = createTextAttributesKey("UNIT_FILE_KEY", DefaultLanguageHighlighterColors.KEYWORD); public static final TextAttributesKey SEPARATOR = createTextAttributesKey("UNIT_FILE_SEPARATOR", DefaultLanguageHighlighterColors.OPERATION_SIGN); - public static final TextAttributesKey VALUE - = createTextAttributesKey("UNIT_FILE_VALUE", DefaultLanguageHighlighterColors.STRING); + public static final TextAttributesKey TEXT + = createTextAttributesKey("UNIT_FILE_TEXT", DefaultLanguageHighlighterColors.STRING); + + public static final TextAttributesKey CONSTANT + = createTextAttributesKey("UNIT_FILE_CONSTANT", DefaultLanguageHighlighterColors.CONSTANT); + + public static final TextAttributesKey NUMBER + = createTextAttributesKey("UNIT_FILE_NUMBER", DefaultLanguageHighlighterColors.NUMBER); + + public static final TextAttributesKey OPERATOR + = createTextAttributesKey("UNIT_FILE_OPERATOR", DefaultLanguageHighlighterColors.OPERATION_SIGN); + private static final TextAttributesKey COMMENT = createTextAttributesKey("UNIT_FILE_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT); private static final TextAttributesKey BAD_CHARACTER @@ -34,10 +44,11 @@ public class UnitFileHighlighter extends SyntaxHighlighterBase { private static final TextAttributesKey[] SECTION_KEYS = new TextAttributesKey[]{SECTION}; private static final TextAttributesKey[] KEY_KEYS = new TextAttributesKey[]{KEY}; private static final TextAttributesKey[] SEPARATOR_KEYS = new TextAttributesKey[]{SEPARATOR}; - private static final TextAttributesKey[] VALUE_KEYS = new TextAttributesKey[]{VALUE}; + private static final TextAttributesKey[] TEXT_KEYS = new TextAttributesKey[]{TEXT}; private static final TextAttributesKey[] COMMENT_KEYS = new TextAttributesKey[]{COMMENT}; private static final TextAttributesKey[] EMPTY_KEYS = new TextAttributesKey[0]; private static final TextAttributesKey[] BAD_CHAR_KEYS = new TextAttributesKey[]{BAD_CHARACTER}; + @NotNull @Override @@ -58,7 +69,7 @@ public TextAttributesKey[] getTokenHighlights(IElementType tokenType) { return KEY_KEYS; } else if (tokenType.equals(UnitFileElementTypeHolder.CONTINUING_VALUE) || tokenType.equals(UnitFileElementTypeHolder.COMPLETED_VALUE)) { - return VALUE_KEYS; + return TEXT_KEYS; } else if (tokenType.equals(UnitFileElementTypeHolder.COMMENT)) { return COMMENT_KEYS; } else if (tokenType.equals(TokenType.BAD_CHARACTER)) { diff --git a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java index e900eb4e..a5fce01e 100644 --- a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java +++ b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java @@ -20,7 +20,10 @@ public class UnitFileColorSettings implements ColorSettingsPage { new AttributesDescriptor("Section", UnitFileHighlighter.SECTION), new AttributesDescriptor("Key", UnitFileHighlighter.KEY), new AttributesDescriptor("Separator", UnitFileHighlighter.SEPARATOR), - new AttributesDescriptor("Value", UnitFileHighlighter.VALUE), + new AttributesDescriptor("Text", UnitFileHighlighter.TEXT), + new AttributesDescriptor("Constant", UnitFileHighlighter.CONSTANT), + new AttributesDescriptor("Number", UnitFileHighlighter.NUMBER), + new AttributesDescriptor("Operator", UnitFileHighlighter.NUMBER), }; @Nullable @@ -38,30 +41,77 @@ public SyntaxHighlighter getHighlighter() { @NotNull @Override public String getDemoText() { - return "# SPDX-License-Identifier: LGPL-2.1+\n" - + "#\n" - + "# This file is part of systemd.\n" - + "#\n" - + "# systemd is free software; you can redistribute it and/or modify it\n" - + "# under the terms of the GNU Lesser General Public License as published by\n" - + "# the Free Software Foundation; either version 2.1 of the License, or\n" - + "# (at your option) any later version.\n" - + "\n" - + "[Unit]\n" - + "Description=Reload Configuration from the Real Root\n" - + "DefaultDependencies=no\n" - + "Requires=initrd-root-fs.target\n" - + "After=initrd-root-fs.target\n" - + "OnFailure=emergency.target\n" - + "OnFailureJobMode=replace-irreversibly\n" - + "ConditionPathExists=/etc/initrd-release\n" - + "\n" - + "[Service]\n" - + "Type=oneshot\n" - + "ExecStartPre=-/usr/bin/systemctl daemon-reload\n" - + "; we have to retrigger initrd-fs.target after daemon-reload\n" - + "ExecStart=-/usr/bin/systemctl --no-block start initrd-fs.target\n" - + "ExecStart=/usr/bin/systemctl --no-block start initrd-cleanup.service\n"; + // language="unit file (systemd)" + return """ + # /etc/systemd/system/webapp.service + # Comprehensive systemd unit for a Python-based web application + + [Unit] + Description=Example Python Web Application + Documentation=https://example.com/docs/webapp + After=network.target postgresql.service + Requires=postgresql.service + + [Service] + Type=simple + + # Start the Python web app + ExecStart=/usr/bin/python3 /opt/webapp/app.py --port=8080 --env=production + + # Reload the app if it supports SIGHUP + ExecReload=/bin/kill -HUP $MAINPID + + # Gracefully stop the app + ExecStop=/bin/kill -TERM $MAINPID + + # Drop privileges + User=webapp + Group=webapp + + # Set working directory + WorkingDirectory=/opt/webapp + + # Environment configuration + Environment=APP_ENV=production + Environment=PORT=8080 + EnvironmentFile=-/etc/webapp/env + + # File descriptor and memory limits + LimitNOFILE=65536 + MemoryMax=1G + + # Restart logic + Restart=on-failure + RestartSec=3s + + # Security hardening + NoNewPrivileges=true + ProtectSystem=full + ProtectHome=yes + PrivateTmp=true + PrivateDevices=true + ProtectControlGroups=true + ProtectKernelModules=true + CapabilityBoundingSet= + RestrictRealtime=true + + # Network access control + IPAddressAllow=10.0.0.0/16 + IPAddressDeny=any + + # Logging + StandardOutput=journal + StandardError=journal + SyslogIdentifier=webapp + + # Uncomment below if your app uses systemd notifications + # Type=notify + # WatchdogSec=60 + + [Install] + WantedBy=multi-user.target + Alias=webapp.service + """; } @Nullable diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/GrammarOptionValueAnnotator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/GrammarOptionValueAnnotator.kt new file mode 100644 index 00000000..bde8d7fe --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/GrammarOptionValueAnnotator.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.annotators + +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +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.UnitFileSectionGroups +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.SemanticDataRepository +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.fileClass + +class GrammarOptionValueAnnotator : Annotator { + + override fun annotate(property: PsiElement, holder: AnnotationHolder) { + if (property is UnitFileProperty) { + val section = PsiTreeUtil.getParentOfType(property, UnitFileSectionGroups::class.java) ?: return + property.valueText ?: return + + val fileClass = section.containingFile.fileClass() + val ovi = SemanticDataRepository.instance.getOptionValidator(fileClass, section.sectionName, property.key) + + ovi.highlight(property, holder) + + + + } + } + + companion object { + const val ANNOTATION_ERROR_MSG = "PID files should be avoided in modern projects. Use Type=notify, Type=notify-reload or Type=simple where possible, which does not require use of PID files to determine the main process of a service and avoids needless forking." + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt new file mode 100644 index 00000000..ef2f0642 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt @@ -0,0 +1,88 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil +import net.sjrx.intellij.plugins.systemdunitfiles.intentions.AddPropertyAndValueQuickFix +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.intentions.AddPropertyQuickFix +import java.util.* + +/** + * This inspection warns when IPAddressAllow is specified without IPAddressDeny in certain sections, + * as this configuration does not block traffic (the default action is to permit). + */ +class IPAddressAllowOnlyInspection : LocalInspectionTool() { + + // Sections where this inspection applies + private val targetSections = setOf("Slice", "Scope", "Service", "Socket") + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : UnitFileVisitor() { + // Map to track properties by section + private val sectionProperties = mutableMapOf>() + + override fun visitFile(file: PsiFile) { + super.visitFile(file) + + // After visiting the file, check each section for the condition + for ((section, properties) in sectionProperties) { + if (section in targetSections && + "IPAddressAllow" in properties && + "IPAddressDeny" !in properties) { + + // Find all IPAddressAllow properties in this section to highlight + val sectionElement = findSectionElement(file, section) + if (sectionElement != null) { + val ipAddressAllowProperties = findPropertiesInSection(sectionElement, "IPAddressAllow") + + for (property in ipAddressAllowProperties) { + holder.registerProblem( + property, + "Specifying IPAddressAllow without IPAddressDeny does not block traffic as the default action is to permit", + com.intellij.codeInspection.ProblemHighlightType.WEAK_WARNING, + AddPropertyAndValueQuickFix(section, "IPAddressDeny", "any") + ) + } + } + } + } + } + + override fun visitPropertyType(property: UnitFilePropertyType) { + super.visitPropertyType(property) + + val section = PsiTreeUtil.getParentOfType(property, UnitFileSectionGroups::class.java) + if (section != null && section.sectionName in targetSections) { + // Add this property to the section's property set + val sectionName = section.sectionName + if (!sectionProperties.containsKey(sectionName)) { + sectionProperties[sectionName] = mutableSetOf() + } + sectionProperties[sectionName]?.add(property.key) + } + } + + /** + * Find a section element by name in the file + */ + private fun findSectionElement(file: PsiFile, sectionName: String): UnitFileSectionGroups? { + val sections = PsiTreeUtil.findChildrenOfType(file, UnitFileSectionGroups::class.java) + return sections.find { it.sectionName == sectionName } + } + + /** + * Find all properties with a specific key in a section + */ + private fun findPropertiesInSection(section: UnitFileSectionGroups, propertyKey: String): List { + val properties = PsiTreeUtil.findChildrenOfType(section, UnitFilePropertyType::class.java) + return properties.filter { it.key == propertyKey } + } + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/AddPropertyAndValueQuickFix.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/AddPropertyAndValueQuickFix.kt new file mode 100644 index 00000000..1741d90a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/AddPropertyAndValueQuickFix.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.intentions + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.pom.Navigatable +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.childrenOfType +import com.intellij.psi.util.endOffset +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionGroups + +class AddPropertyAndValueQuickFix(val section: String, val key: String, val value: String) : LocalQuickFix { + override fun getFamilyName(): String { + return "Add ${key}=${value} to ${section}" + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + + descriptor.psiElement.containingFile ?: return + val sectionGroup = PsiTreeUtil.getParentOfType(descriptor.psiElement, UnitFileSectionGroups::class.java) ?: return + val dummyFile = UnitElementFactory.createFile(project, sectionGroup.text + "\n${key}=${value}") + + val newElement = sectionGroup.replace(dummyFile.firstChild) + + (dummyFile.lastChild.navigationElement as? Navigatable)?.navigate(true) + + FileEditorManager.getInstance(project).selectedTextEditor?.caretModel?.moveToOffset(newElement.endOffset) + } + +} + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt index 48493864..a9539c7e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt @@ -152,6 +152,8 @@ class SemanticDataRepository private constructor() { validatorMap.putAll(CPUSharesOptionValue.validators) validatorMap.putAll(CgroupSocketBindOptionValue.validators) validatorMap.putAll(RlimitOptionValue.validators) // Scopes are not supported since they aren't standard unit files. + validatorMap.putAll(NetworkAddressOptionValue.validators) + validatorMap.putAll(InAddrPrefixesOptionValue.validators) fileClassToSectionNameToKeyValuesFromDoc["unit"]?.remove(SCOPE_KEYWORD) fileClassToSectionToKeyAndValidatorMap["unit"]?.remove(SCOPE_KEYWORD) } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/InAddrPrefixesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/InAddrPrefixesOptionValue.kt new file mode 100644 index 00000000..4a74659d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/InAddrPrefixesOptionValue.kt @@ -0,0 +1,17 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +class InAddrPrefixesOptionValue(combinator: Combinator) : GrammarOptionValue("config_parse_in_addr_prefixes", combinator) { + + companion object { + + val validators = mapOf( + Validator("config_parse_in_addr_prefixes", "AF_UNSPEC") to InAddrPrefixesOptionValue(SequenceCombinator(IP_ADDR_PREFIX_LIST, EOF())), + Validator("config_parse_in_addr_prefixes", "AF_INET") to InAddrPrefixesOptionValue(SequenceCombinator(IPV4_ADDR_PREFIX_LIST, EOF())), + Validator("config_parse_in_addr_prefixes", "AF_INET6") to InAddrPrefixesOptionValue(SequenceCombinator(IPV6_ADDR_PREFIX_LIST, EOF())) + ) + } +} + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt new file mode 100644 index 00000000..c7bac63e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt @@ -0,0 +1,20 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_PREFIX_LENGTH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +class NetworkAddressOptionValue() : GrammarOptionValue("config_parse_address_section", GRAMMAR) { + + companion object { + val GRAMMAR = SequenceCombinator( + IP_ADDR_AND_PREFIX_LENGTH, EOF()) + + val validators = mapOf( + Validator("config_parse_address_section", "ADDRESS_ADDRESS") to NetworkAddressOptionValue() + ) + } +} + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueInformation.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueInformation.kt index d7517b02..84a1d971 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueInformation.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueInformation.kt @@ -2,7 +2,9 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.project.Project +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileProperty import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType interface OptionValueInformation { @@ -33,6 +35,11 @@ interface OptionValueInformation { } } + /** + * highlights the value + */ + fun highlight(property: UnitFileProperty, holder: AnnotationHolder) {} + /** * Get the name of the validator that this implements. * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt index 69d68a58..4ddbd8ac 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt @@ -25,6 +25,8 @@ open class AlternativeCombinator(vararg val tokens: Combinator) : Combinator { longestTokenMatch = match.tokens maxLength = max(maxLength, match.longestMatch) } + + } return MatchResult(longestTokenMatch, -1, longestTerminalMatch, maxLength) @@ -37,4 +39,21 @@ open class AlternativeCombinator(vararg val tokens: Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, Combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("Alt(\n") + for (token in tokens) { + if (token is SequenceCombinator || token is AlternativeCombinator || token is Repeat || token is ZeroOrOne || token is ZeroOrMore || token is OneOrMore) { + sb.append(token.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(token.toString()).append("\n") + } + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AstCombinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AstCombinator.kt new file mode 100644 index 00000000..2c1b6f9d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AstCombinator.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import kotlin.math.max + +class AstCombinator(val type : String, val combinator : Combinator) : Combinator { + + fun match(value: String, offset: Int, f: (Combinator, String, Int) -> MatchResult): MatchResult { + + + var longestTokenMatch = emptyList() + var longestTerminalMatch = emptyList() + var maxLength = 0 + + for (token in tokens) { + val match = f(token, value, offset) + if (match.matchResult != -1) { + return match + } + + if (match.tokens.size > longestTerminalMatch.size) { + longestTerminalMatch = match.terminals + longestTokenMatch = match.tokens + maxLength = max(maxLength, match.longestMatch) + } + + + } + + return MatchResult(longestTokenMatch, -1, longestTerminalMatch, maxLength) + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + return match(value, offset, Combinator::SyntacticMatch) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + return match(value, offset, Combinator::SemanticMatch) + } + + override fun toStringIndented(indent: Int): String { + return "AstCombinator( type=$type,\n" + combinator.toStringIndented(indent + 1) + "\n)" + } + +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt index eac35737..29757dac 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt @@ -32,4 +32,5 @@ interface Combinator { */ fun SemanticMatch(value : String, offset: Int): MatchResult + fun toStringIndented(indent: Int): String } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt index feefbec4..e9dc6012 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt @@ -1,5 +1,99 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar +import net.sjrx.intellij.plugins.systemdunitfiles.coloring.UnitFileHighlighter +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFile + val BYTES = RegexTerminal("[0-9]+[a-zA-Z]*\\s*", "[0-9]+[KMGT]?\\s*") val DEVICE = RegexTerminal("\\S+\\s*", "/[^\\u0000. ]+\\s*") val IOPS = RegexTerminal("[0-9]+[a-zA-Z]*\\s*", "[0-9]+[KMGT]?\\s*") + +var IPV4_OCTET = IntegerTerminal(0, 256) +val DOT = LiteralChoiceTerminal(".") +var IPV4_ADDR = HighlightCombinator(SequenceCombinator(IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET), UnitFileHighlighter.NUMBER) + +val CIDR_SEPARATOR = LiteralChoiceTerminal("/") + +val IPV4_ADDR_AND_PREFIX_LENGTH = SequenceCombinator(IPV4_ADDR, CIDR_SEPARATOR, IntegerTerminal(8, 33)) +val IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH = SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator( CIDR_SEPARATOR, IntegerTerminal(8, 33)))) + +var IPV6_HEXTET = RegexTerminal("[0-9a-fA-F]{1,4}", "[0-9a-fA-F]{1,4}") +val COLON = LiteralChoiceTerminal(":") +val DOUBLE_COLON = LiteralChoiceTerminal("::") + + +val IPV6_FULL_SPECIFIED = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET) +val IPV6_ZERO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 8), IPV6_HEXTET))) +val IPV6_ONE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator( Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 7), IPV6_HEXTET))) +val IPV6_TWO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 6), IPV6_HEXTET))) +val IPV6_THREE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 5), IPV6_HEXTET))) +val IPV6_FOUR_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 4), IPV6_HEXTET))) +val IPV6_FIVE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 3), IPV6_HEXTET))) +val IPV6_SIX_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 2), IPV6_HEXTET))) +val IPV6_SEVEN_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(IPV6_HEXTET)) + + +val IPV6_IPV4_SUFFIX_FULL = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV4_ADDR) +val IPV6_IPV4_SUFFIX_ZERO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(DOUBLE_COLON,SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 6), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_ONE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, DOUBLE_COLON, SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 5), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_TWO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 4), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_THREE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON,SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 3), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_FOUR_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 2), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_FIVE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, IPV4_ADDR) + +//val IPV6_ALL_ZEROS = DOUBLE_COLON + +val IPV6_ADDR = HighlightCombinator(AlternativeCombinator( + IPV6_IPV4_SUFFIX_FULL, + IPV6_IPV4_SUFFIX_ZERO_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_ONE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FULL_SPECIFIED, + IPV6_SEVEN_HEXTET_BEFORE_ZERO_COMP, + IPV6_SIX_HEXTET_BEFORE_ZERO_COMP, + IPV6_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_ONE_HEXTET_BEFORE_ZERO_COMP, + // Must go last because it's the most general and can match :: + IPV6_ZERO_HEXTET_BEFORE_ZERO_COMP, + + // I suspect maybe that this one is redundant + //IPV6_ALL_ZEROS, +), UnitFileHighlighter.NUMBER) + +val IPV6_ADDR_AND_PREFIX_LENGTH = SequenceCombinator(IPV6_ADDR, CIDR_SEPARATOR, IntegerTerminal(64, 129)) +val IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH = SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, IntegerTerminal(64, 129)))) + + +var IP_ADDR_AND_PREFIX_LENGTH = AlternativeCombinator( + IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IPV6_ADDR_AND_PREFIX_LENGTH) + +var IN_ADDR_PREFIX_SPECIAL_VALUES = LiteralChoiceTerminal("any", "localhost", "link-local", "multicast") + +var IPV4_ADDR_AND_PREFIX_OR_SPECIAL = AlternativeCombinator( + IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IN_ADDR_PREFIX_SPECIAL_VALUES, +) + +var IPV6_ADDR_AND_PREFIX_OR_SPECIAL = AlternativeCombinator( + IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IN_ADDR_PREFIX_SPECIAL_VALUES, +) + + +var IP_ADDR_AND_PREFIX_OR_SPECIAL = AlternativeCombinator( + IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IN_ADDR_PREFIX_SPECIAL_VALUES, +) + +var IP_ADDR_PREFIX_LIST = SequenceCombinator(IP_ADDR_AND_PREFIX_OR_SPECIAL, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), IP_ADDR_AND_PREFIX_OR_SPECIAL))) +var IPV4_ADDR_PREFIX_LIST = SequenceCombinator(IPV4_ADDR_AND_PREFIX_OR_SPECIAL, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), IPV4_ADDR_AND_PREFIX_OR_SPECIAL))) +var IPV6_ADDR_PREFIX_LIST = SequenceCombinator(IPV6_ADDR_AND_PREFIX_OR_SPECIAL, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), IPV6_ADDR_AND_PREFIX_OR_SPECIAL))) + + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt index c0316686..770b4ef7 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt @@ -16,4 +16,8 @@ class EOF : Combinator { NoMatch } } + + override fun toStringIndented(indent: Int): String { + return "EOF" + } } 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 6e94fa00..4876bfde 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 @@ -91,5 +91,15 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin return NoMatch.copy(longestMatch = offset) } + override fun toString(): String { + return if (choices.size == 1) { + "Literal(\"${choices[0]}\")" + } else { + "FlexLitChoice(" + choices.joinToString(",") { "\"$it\"" } + ")" + } + } + override fun toStringIndented(indent: Int): String { + return toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt index 68bb0aba..f7f82b8e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt @@ -3,10 +3,13 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gra import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.HighlightSeverity import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import net.sjrx.intellij.plugins.systemdunitfiles.intentions.ReplaceInvalidLiteralChoiceQuickFix +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileProperty import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.SemanticDataRepository import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.OptionValueInformation @@ -53,6 +56,7 @@ open class GrammarOptionValue( return } + val semanticMatch = combinator.SemanticMatch(value, 0) if (semanticMatch.matchResult == -1) { @@ -101,6 +105,7 @@ open class GrammarOptionValue( + return } @@ -111,6 +116,26 @@ open class GrammarOptionValue( return + } + + override fun highlight(property: UnitFileProperty, holder: AnnotationHolder) { + val value = property.valueText ?: return + + val syntaticMatch = combinator.SyntacticMatch(value, 0) + + try { + if (syntaticMatch.matchResult >= 0) { + //for ( (k,v) in syntaticMatch.highlights) { + + property + holder.newSilentAnnotation(HighlightSeverity.INFORMATION).range(TextRange(property.valueNode.textRange.startOffset + k.startOffset, property.valueNode.textRange.startOffset + k.endOffset)).textAttributes(v).create() + //} //syntaticMatch.highlights + } + } catch(e : RuntimeException) { + LOG.error("Error while processing ${property.key} with value ${value}", e) + } + + } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt index a9b3e6e6..f1107238 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt @@ -22,4 +22,8 @@ class IntegerTerminal(private val minInclusive: Int,private val maxExclusive: In return MatchResult(listOf(matchResult.value), offset + matchResult.value.length, listOf(this), offset + matchResult.value.length) } + + override fun toString(): String { + return "Int($minInclusive,$maxExclusive)" + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt index 08ee0d2a..870db3d1 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt @@ -23,4 +23,12 @@ class LiteralChoiceTerminal(vararg var choices: String) : TerminalCombinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset) } + + override fun toString(): String { + return if (choices.size == 1) { + "Literal(\"${choices[0]}\")" + } else { + "LitChoice(" + choices.joinToString(",") { "\"$it\"" } + ")" + } + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/MatchResult.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/MatchResult.kt index c0a51a32..2c0743a4 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/MatchResult.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/MatchResult.kt @@ -1,6 +1,16 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar -data class MatchResult(val tokens: List, val matchResult: Int, val terminals: List, val longestMatch : Int ) { +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.util.TextRange + +data class Range(val start: Int, val end: Int) + +data class Highlight(val start: Int, val end: Int, ) + +data class AstNode(val type: String, val text: String, val children: List = emptyList()) +// One day we should remove the dependencies on intellij. Maybe change the grammar to build an AST. + +data class MatchResult(val tokens: List, val matchResult: Int, val terminals: List, val longestMatch : Int, val astNode : AstNode? = null) { init { if (tokens.size != terminals.size) { throw IllegalArgumentException("Tokens and terminals must be the same size, ${tokens.size} != ${terminals.size}") diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt index 476dfb82..f56a0f1b 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt @@ -39,4 +39,19 @@ class OneOrMore(val combinator : Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("OneOrMore(\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt index 53fab5ec..c7576495 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt @@ -5,30 +5,9 @@ class OptionalWhitespacePrefix(val combinator: Combinator): SequenceCombinator(WhitespaceTerminal(), combinator), combinator ) { -// -// override fun SyntacticMatch(value: String, offset: Int): MatchResult { -// var newOffset = offset -// for(o in offset..= 0") + } + if (maxExclusive < minInclusive) { + throw IllegalArgumentException("maxExclusive must be >= minInclusive") + } + } + + + private fun match(value: String, offset: Int, f: (String, Int) -> MatchResult): MatchResult { + var index = offset + val tokens = mutableListOf() + val terminals = mutableListOf() + + var match = f(value, index) + var matches = 0 + + if (match.matchResult == -1) { + if (minInclusive != 0) { + // This will return a match result = -1 + return match + } + + return MatchResult(tokens, offset, terminals, match.longestMatch) + } + + var maxLength = match.longestMatch + + + while (match.matchResult != -1 && matches < maxExclusive) { + matches++ + index = match.matchResult + tokens.addAll(match.tokens) + terminals.addAll(match.terminals) + + match = f(value, index) + maxLength = max(maxLength, match.longestMatch) + } + + if (matches < minInclusive) { + return MatchResult(emptyList(), -1, emptyList(), maxLength) + } else { + return MatchResult(tokens, index, terminals, maxLength) + } + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + return match(value, offset, combinator::SyntacticMatch) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + return match(value, offset, combinator::SemanticMatch) + } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("Repeat($minInclusive,$maxExclusive\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt index 8d674d75..704dc360 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt @@ -7,12 +7,14 @@ import kotlin.math.max */ open class SequenceCombinator(vararg val tokens: Combinator) : Combinator { - override fun SyntacticMatch(value: String, offset: Int): MatchResult { + override fun SyntacticMatch(value: String,offset: Int): MatchResult { var index = offset val resultTokens = mutableListOf() val resultTerminals = mutableListOf() var maxLength = 0 + val children = ArrayList() + for (token in tokens) { val match = token.SyntacticMatch(value, index) @@ -28,7 +30,8 @@ open class SequenceCombinator(vararg val tokens: Combinator) : Combinator { } - return MatchResult(resultTokens, index, resultTerminals, maxLength) + + return MatchResult(resultTokens, index, resultTerminals, maxLength, AstNode( type="Seq", value.substring(offset, maxLength), children)) } override fun SemanticMatch(value: String, offset: Int): MatchResult { @@ -54,4 +57,21 @@ open class SequenceCombinator(vararg val tokens: Combinator) : Combinator { } return MatchResult(resultTokens, index, resultTerminals, maxLength) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("Seq(\n") + for (token in tokens) { + if (token is SequenceCombinator || token is AlternativeCombinator || token is Repeat || token is ZeroOrOne || token is ZeroOrMore || token is OneOrMore) { + sb.append(token.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(token.toString()).append("\n") + } + } + sb.append(prefix).append(")") + return sb.toString() + } } 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 8e01cf76..8d1705af 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 @@ -1,3 +1,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar -interface TerminalCombinator : Combinator +interface TerminalCombinator : Combinator { + override fun toStringIndented(indent: Int): String { + return toString() + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt index aa932e22..180e5eb4 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt @@ -26,4 +26,12 @@ class WhitespaceTerminal : TerminalCombinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset) } + + override fun toString(): String { + return "\\s+" + } + + override fun toStringIndented(indent: Int): String { + return toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt index f935ea48..beeced13 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt @@ -41,4 +41,19 @@ class ZeroOrMore(val combinator : Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("ZeroOrMore(\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt index 7db1faef..1751b24c 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt @@ -39,4 +39,19 @@ class ZeroOrOne(val combinator : Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("ZeroOrOne(\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 86d8fa14..34592ae3 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -60,6 +60,7 @@ + + diff --git a/src/main/resources/inspectionDescriptions/IPAddressAllowOnly.html b/src/main/resources/inspectionDescriptions/IPAddressAllowOnly.html new file mode 100644 index 00000000..9a7dcc17 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/IPAddressAllowOnly.html @@ -0,0 +1,22 @@ + + + +

This inspection warns when IPAddressAllow= is specified without IPAddressDeny= in Slice, Scope, Service, or Socket sections.

+

When IPAddressAllow= is used without IPAddressDeny=, it does not block traffic as the default action is to permit.

+

To properly restrict traffic, both IPAddressAllow= and IPAddressDeny= should be specified.

+ +

Note: This warning may be safely ignored in cases where:

+
    +
  • The unit is part of a slice that already has IPAddressDeny= configured at a higher level
  • +
  • The unit inherits IP access restrictions from a parent slice unit (such as -.slice or system.slice)
  • +
  • You are implementing a hierarchical firewall structure where IPAddressDeny=any is set on an upper-level slice unit, and individual services only need to specify IPAddressAllow= entries
  • +
+ +

systemd applies IP access lists hierarchically, combining lists from parent slice units with those defined in the current unit. The access control rules are applied in this order:

+
    +
  1. Access is granted when the IP address matches an entry in IPAddressAllow=
  2. +
  3. Otherwise, access is denied when the IP address matches an entry in IPAddressDeny=
  4. +
  5. Otherwise, access is granted
  6. +
+ + diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/OptionValueAnnotationTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/OptionValueAnnotationTest.kt new file mode 100644 index 00000000..e159a0af --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/OptionValueAnnotationTest.kt @@ -0,0 +1,22 @@ +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class OptionValueAnnotationTest : AbstractUnitFileTest() { + fun testIPAddressIsHighlightedCorrectly() { + // language="unit file (systemd)" + val file = """ + [Service] + IPAddressAllow=127.0.0.1 + Type=oneshot + ExecStart=/usr/bin/cowsay + """.trimIndent() + + + setupFileInEditor("file.service", file) + val highlights = myFixture.doHighlighting() + + /* + * Verification + */ + assertSize(1, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt new file mode 100644 index 00000000..79ecaa94 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class IPAddressAllowOnlyInspectionTest: AbstractUnitFileTest() { + + fun testServiceThrowsWarningWhenIPAddressAllowIsUsedByNoIPAddressDeny() { + + // Fixture Setup + // language="unit file (systemd)" + val file = """ + # SPDX-License-Identifier: LGPL-2.1-or-later + [Unit] + Description=test + + [Service] + IPAddressAllow=192.168.0.0/24 + + """.trimIndent() + + // Exercise SUT + setupFileInEditor("file.service", file) + enableInspection(IPAddressAllowOnlyInspection::class.java) + + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + } + + fun testServiceThrowsNoWarningWhenIPAddressAllowIsUsedByIPAddressDeny() { + + // Fixture Setup + // language="unit file (systemd)" + val file = """ + # SPDX-License-Identifier: LGPL-2.1-or-later + [Unit] + Description=test + + [Service] + IPAddressAllow=192.168.0.0/24 + IPAddressDeny=any + + """.trimIndent() + + // Exercise SUT + setupFileInEditor("file.service", file) + enableInspection(IPAddressAllowOnlyInspection::class.java) + + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InAddrPrefixOptionalValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InAddrPrefixOptionalValueTest.kt new file mode 100644 index 00000000..922efb87 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InAddrPrefixOptionalValueTest.kt @@ -0,0 +1,121 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InAddrPrefixOptionalValueTest : AbstractUnitFileTest() { + + fun testNoWarningWhenValidIPAddressesInServiceSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Service] + IPAddressAllow=244.178.44.111/32 + IPAddressAllow=any + IPAddressAllow=localhost + IPAddressAllow=link-local + IPAddressAllow=multicast + IPAddressAllow=127.0.0.1 + IPAddressAllow=1.2.3.4 5.6.7.8/23 + IPAddressAllow=::FFFF/64 + IPAddressAllow=::/64 + IPAddressAllow=::1/128 + IPAddressAllow=2001:db8::1/128 2001:db8:ABCD:0012::0/96 2001:0db8:85a3:0000:0000:8a2e:0370:7334 multicast + IPAddressAllow=fe80::/64 + IPAddressAllow=::/128 + IPAddressAllow=::ffff:192.0.2.128/96 + IPAddressAllow=2001:0db8:85a3::8a2e:0370:7334/124 + IPAddressDeny=any + IPAddressDeny=localhost + IPAddressDeny=link-local + IPAddressDeny=multicast + IPAddressDeny=64.2.3.4 1.2.3.5 6.9.0.1 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningsWhenInvalidIPAddressesInServiceSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Service] + # Not a valid option + IPAddressAllow=all + # First octet is too high + IPAddressAllow=256.178.44.111 + # Too few octets + IPAddressAllow=245.124.2/12 + # Too many octets + IPAddressDeny=1.2.3.4.5/8 + # Invalid Prefix Length x 2 but valid IPs in between + IPAddressAllow=244.25.2.1/33 4.2.3.5 244.25.2.1/7 any + IPAddressDeny=2001:0db8:85a3:0000:0000:8a2e:0370:7334:1234/64 + # Too few hextets (only 2) + # Invalid character (g is not a hex digit) + IPAddressDeny=abcd:1234/64 2001:db8:85a3:0:0:8a2e:370g:7334/64 2001:db8::85a3::7334/64 + # Prefix too large (>128) + IPAddressDeny=2001:db8::1/129 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(8, highlights) + } + + fun testNoWarningWhenValidIPv4AndIPv6AddressesSetInNetwork() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [DHCPv4] + AllowList=1.2.3.4/32 + AllowList=any + + [IPv6AcceptRA] + RouterAllowList=2001:0db8:85a3::8a2e:0370:7334/124 + RouterAllowList=link-local + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningWhenValidIPv4AndIPv6AddressesSetInNetwork() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [DHCPv4] + AllowList=2001:0db8:85a3::8a2e:0370:7334/124 + AllowList=any + + [IPv6AcceptRA] + RouterAllowList=1.2.3.4 + RouterAllowList=link-local + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(2, highlights) + } + + + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt new file mode 100644 index 00000000..0f3656d0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt @@ -0,0 +1,138 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InvalidValueForNetworkAddressesTest : AbstractUnitFileTest() { + + fun testNoWarningWhenValidIPv4NetworkAddressSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Network] + Address=244.178.44.111/32 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningWhenSomeInvalidIPv4NetworkAddressSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Network] + # The first octet is two high + Address=256.178.44.111 + # Too few octets + Address=245.124.2/12 + # Too many octets + Address=1.2.3.4.5/8 + # Invalid Prefix Length + Address=244.25.2.1/33 + # Invalid Prefix Length + Address=244.25.2.1/7 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(5, highlights) + + } + + fun testNoWarningWhenValidIPv6NetworkAddressSet() { + // Fixture Setup + + // language="unit file (systemd)" + val file=""" + [Network] + Address=::ffff/64 + Address=::FFFF/64 + Address=::/64 + Address=::1/128 + Address=2001:db8::1/128 + Address=2001:db8:0:0:0:0:2:1/127 + Address=2001:db8:ABCD:0012::0/96 + Address=fe80::/64 + Address=::/128 + Address=::ffff:192.0.2.128/96 + Address=2001:0db8:85a3:0000:0000:8a2e:0370:7334/124 + Address=2001:0db8:85a3::8a2e:0370:7334/124 + Address=fd00:1234:5678:9abc:def0:1234:5678:9abc/100 + Address=2001:db8:0:0:0:0:0:1/126 + Address=2001:db8:85a3::8a2e:370:7334/120 + Address=::ABCD/112 + Address=::1/127 + Address=2001:db8::/65 + Address=ff02::1/128 + # Honestly I don't know what matches this + Address=2001:0db8:85a3:0000:0000:8a2e:192.168.0.1/96 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningWhenSomeInvalidIPv6NetworkAddressSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Network] + # Too many hextets (9 instead of max 8) + Address=2001:0db8:85a3:0000:0000:8a2e:0370:7334:1234/64 + # Too few hextets (only 2) + Address=abcd:1234/64 + # Invalid character (g is not a hex digit) + Address=2001:db8:85a3:0:0:8a2e:370g:7334/64 + # Double '::' not allowed + Address=2001:db8::85a3::7334/64 + # Prefix too large (>128) + Address=2001:db8::1/129 + # Prefix too small (<0) + Address=2001:db8::1/-1 + # Missing prefix + Address=2001:db8::1 + # Empty address + Address=/64 + # Trailing colon + Address=2001:db8:85a3:0:0:8a2e:370:7334:/64 + # Leading colon (not part of '::') + Address=:2001:db8:85a3:0:0:8a2e:370:7334/64 + # Too many consecutive colons (illegal ':::') + Address=2001:db8:::1/64 + # Embedded IPv4 with too many octets + Address=::ffff:192.168.1.1.1/96 + # Embedded IPv4 with invalid octet (>255) + Address=::ffff:300.168.1.1/96 + # Hextet too long (more than 4 hex digits) + Address=2001:db8:12345::1/64 + # Non-hex character in hextet + Address=2001:db8:zzzz::1/64 + # Invalid number of octets (missing one with no zero compression) + Address=2001:0db8:85a3:0000:0000:192.168.0.1/96 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(16, highlights) + + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt index 15f9eb70..84ecab43 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt @@ -827,4 +827,168 @@ class GrammarTest : TestCase() { assertEquals(NoMatch, optionalWhitespacePrefix.SyntacticMatch(invalidFromOffset, garbage.length)) } + fun testRepeatCombinatorMatchesNonZeroMin() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + val fizzOrBuzz = RegexTerminal("[a-z]{4}", "fizz|buzz") + + val repeatCombinator = Repeat(fizzOrBuzz, 2, 4) + + val semValid = "fizzbuzzfizz" + val synValid = "blehblehbleh" + //val invalid = "fizz" + val tooShort = "fizz" + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val synValidFromOffset = "${garbage}${synValid}" + val tooShortFromOffset = "${garbage}${tooShort}" + + /** + * Execute SUT & Verification + */ + var match = repeatCombinator.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValid, 0) + assertEquals(synValid.length, match.matchResult) + assertEquals(listOf("bleh", "bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, repeatCombinator.SemanticMatch(synValid, 0)) + + match = repeatCombinator.SyntacticMatch(tooShort, 0) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(4, match.longestMatch) + + match = repeatCombinator.SemanticMatch(tooShort, 0) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(4, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValidFromOffset, garbage.length) + assertEquals(synValidFromOffset.length, match.matchResult) + assertEquals(listOf("bleh", "bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, repeatCombinator.SemanticMatch(synValidFromOffset, garbage.length)) + + match = repeatCombinator.SyntacticMatch(tooShortFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(6, match.longestMatch) + + match = repeatCombinator.SemanticMatch(tooShortFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(6, match.longestMatch) + + } + + fun testRepeatCombinatorMatchesZeroMinAndExtraAtEnd() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + val fizzOrBuzz = RegexTerminal("[a-z]{4}", "fizz|buzz") + + val repeatCombinator = Repeat(fizzOrBuzz, 0, 2) + + val semValid = "fizzbuzzfizz" + val synValid = "blehblehbleh" + //val invalid = "fizz" + val emptyString = "" + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val synValidFromOffset = "${garbage}${synValid}" + val emptyStringFromOffset = "${garbage}${emptyString}" + + /** + * Execute SUT & Verification + */ + var match = repeatCombinator.SyntacticMatch(semValid, 0) + assertEquals(8, match.matchResult) + assertEquals(listOf("fizz", "buzz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValid, 0) + assertEquals(8, match.matchResult) + assertEquals(listOf("fizz", "buzz" ), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValid, 0) + assertEquals(8, match.matchResult) + assertEquals(listOf("bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(synValid, 0) + assertEquals(0, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(emptyString, 0) + assertEquals(0, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SemanticMatch(emptyString, 0) + assertEquals(0, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(10, match.matchResult) + assertEquals(listOf("fizz", "buzz" ), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(10, match.matchResult) + assertEquals(listOf("fizz", "buzz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValidFromOffset, garbage.length) + assertEquals(10, match.matchResult) + assertEquals(listOf("bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(synValidFromOffset, garbage.length) + assertEquals(2, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(emptyStringFromOffset, garbage.length) + assertEquals(2, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SemanticMatch(emptyStringFromOffset, garbage.length) + assertEquals(2, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + } + }