diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/ReplaceInvalidLiteralChoiceQuickFix.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/ReplaceInvalidLiteralChoiceQuickFix.kt new file mode 100644 index 00000000..2aac5f70 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/ReplaceInvalidLiteralChoiceQuickFix.kt @@ -0,0 +1,28 @@ +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 + +class ReplaceInvalidLiteralChoiceQuickFix(val offset: Int, val invalidToken : String, val replacementToken : String) : LocalQuickFix { + + override fun getName(): String { + return "Replace '${invalidToken}' with '${replacementToken}'" + } + + override fun getFamilyName(): String { + return "Replace invalid value" + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val fullPropertyValue = descriptor.psiElement.text + + val newText = fullPropertyValue.substring(0, offset) + replacementToken + fullPropertyValue.substring(offset+invalidToken.length) + val newElement = UnitElementFactory.createProperty(project, (descriptor.psiElement.parent as UnitFilePropertyImpl).key, newText) + val property = PsiTreeUtil.getParentOfType(descriptor.psiElement, UnitFilePropertyImpl::class.java)?: return + + property.replace(newElement) + } +} 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 b5854de7..69b60c0f 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 @@ -88,6 +88,11 @@ class SemanticDataRepository private constructor() { validatorMap.putAll(TtySizeOptionValue.validators) validatorMap.putAll(ExecDirectoriesOptionValue.validators) + validatorMap.putAll(IOLimitOptionValue.validators) + validatorMap.putAll(ImagePolicyOptionValue.validators) + validatorMap.putAll(CPUWeightOptionValue.validators) + validatorMap.putAll(CPUSharesOptionValue.validators) + // Scopes are not supported since they aren't standard unit files. sectionNameToKeyValuesFromDoc.remove(SCOPE_KEYWORD) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/CPUSharesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/CPUSharesOptionValue.kt new file mode 100644 index 00000000..b530c6cc --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/CPUSharesOptionValue.kt @@ -0,0 +1,22 @@ +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 CPUSharesOptionValue : GrammarOptionValue(validatorName, GRAMMAR) { + + companion object { + val validatorName = "config_parse_cpu_shares" + + val GRAMMAR = + SequenceCombinator( + IntegerTerminal(2, 262145), + EOF() + ) + + val validators = mapOf( + Validator(validatorName, "0") to CPUSharesOptionValue() + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/CPUWeightOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/CPUWeightOptionValue.kt new file mode 100644 index 00000000..4776f9e8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/CPUWeightOptionValue.kt @@ -0,0 +1,25 @@ +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 CPUWeightOptionValue : GrammarOptionValue(validatorName, GRAMMAR) { + + companion object { + val validatorName = "config_parse_cg_cpu_weight" + + val GRAMMAR = + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("idle"), + IntegerTerminal(1, 10001) + ), + EOF() + ) + + val validators = mapOf( + Validator(validatorName, "0") to CPUWeightOptionValue() + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/IOLimitOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/IOLimitOptionValue.kt new file mode 100644 index 00000000..c5d9531b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/IOLimitOptionValue.kt @@ -0,0 +1,18 @@ +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 IOLimitOptionValue() : GrammarOptionValue("config_parse_io_limit", GRAMMAR) { + + companion object { + val GRAMMAR = SequenceCombinator(OneOrMore(SequenceCombinator(DEVICE, BYTES)), EOF()) + + val validators = mapOf( + Validator("config_parse_io_limit", "0") to IOLimitOptionValue() + ) + } +} + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ImagePolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ImagePolicyOptionValue.kt new file mode 100644 index 00000000..956ca5b6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ImagePolicyOptionValue.kt @@ -0,0 +1,33 @@ +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 ImagePolicyOptionValue : GrammarOptionValue(validatorName, IMAGE_POLICY_COMBINATOR) { + + companion object { + val validatorName = "config_parse_image_policy" + + // Image Polcies + // https://www.freedesktop.org/software/systemd/man/latest/systemd.image-policy.html + val IMAGE_POLICY_SEPARATOR = LiteralChoiceTerminal(":") + val PARTITION_IDENTIFIER = FlexibleLiteralChoiceTerminal( "root", "usr", "home", "srv", "esp", "xbootldr", "swap", "root-verity", "root-verity-sig", "usr-verity", "usr-verity-sig", "tmp", "var") + val PARTITION_POLICY_FLAG = FlexibleLiteralChoiceTerminal("unprotected", "verity", "signed", "encrypted", "unused", "absent", "read-only-off", "read-only-on", "growfs-off", "growfs-on") + val PARTITION_POLICY_FLAG_SEPARATOR = LiteralChoiceTerminal("+") + val PARTITION_IDENTIFIER_FLAG_SEPARATOR = LiteralChoiceTerminal("=") + + + val PARTITION_POLICY_FLAGS = SequenceCombinator(PARTITION_POLICY_FLAG, ZeroOrMore(SequenceCombinator(PARTITION_POLICY_FLAG_SEPARATOR, PARTITION_POLICY_FLAG))) + + + val SINGLE_EXPLICIT_IMAGE_POLICY = SequenceCombinator(PARTITION_IDENTIFIER, PARTITION_IDENTIFIER_FLAG_SEPARATOR, PARTITION_POLICY_FLAGS) + val DEFAULT_IMAGE_POLICY = SequenceCombinator(PARTITION_POLICY_FLAG_SEPARATOR, PARTITION_POLICY_FLAGS) + var IMAGE_POLICY= AlternativeCombinator(SINGLE_EXPLICIT_IMAGE_POLICY, DEFAULT_IMAGE_POLICY) + val IMAGE_POLICY_COMBINATOR = SequenceCombinator(IMAGE_POLICY, ZeroOrMore(SequenceCombinator(IMAGE_POLICY_SEPARATOR, IMAGE_POLICY)), EOF()) + + val validators = mapOf( + Validator(validatorName, "0") to ImagePolicyOptionValue() + ) + } + +} 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 new file mode 100644 index 00000000..444a3010 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import kotlin.math.max + +/** + * This is a sequence of tokens that must match any of them. + */ +class AlternativeCombinator(vararg val tokens: 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) + } +} 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 new file mode 100644 index 00000000..eac35737 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +interface Combinator { + /** + * WARNING: At the current time this combinator implementation doesn't necessarily guarantee a match. + * + * Seq(ZeroOrMore(Literal("fizz")), Literal("fizz")) + * + * If you try and match "fizz", the ZeroOrMore would greedily consume the fizz, and the second wouldn't match. + * + * I'm unclear if this will actually be a problem, and whether it's worth fixing. + */ + + /** + * This checks the value string, starting at offset for a syntactic match. + * + * In a nutshell a syntactic match might accept things that we should color and try and analyze + * but might be incorrect. + * + * For example if you something accepts a positive number, a syntactic regex should match any number even negative or floats + * + * The return value is -1 for no match, or a new offset if this token matched something. + */ + fun SyntacticMatch(value : String, offset: Int): MatchResult + + /** + * This checks the value string, starting at offset for a semantic match. + * + * In a nutshell a semantic match means we understood and it valid as far as the grammar is concerned. + * + * The return value is -1 for no match, or a new offset if this token matched something. + */ + fun SemanticMatch(value : String, offset: Int): MatchResult + +} 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 new file mode 100644 index 00000000..feefbec4 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt @@ -0,0 +1,5 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +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*") 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 new file mode 100644 index 00000000..c0316686 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt @@ -0,0 +1,19 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +class EOF : Combinator { + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + return if (offset == value.length) { + MatchResult(emptyList(), offset, emptyList(), value.length) + } else { + NoMatch + } + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + return if (offset == value.length) { + MatchResult(emptyList(), offset, emptyList(), value.length) + } else { + NoMatch + } + } +} 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 new file mode 100644 index 00000000..6e94fa00 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt @@ -0,0 +1,95 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import kotlin.math.max + +class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombinator { + + init { + choices.sortBy { -it.length } + } + + val syntaticMatch: Regex + + init { + + var dash = false + var specialChars = "" + var lowerCase = false + var upperCase = false + var numbers = false + var maxLength = 0 + + for (choice in choices) { + maxLength = max(maxLength, choice.length) + for (char in choice) { + if (char in 'a'..'z') { + lowerCase = true + continue + } + + if (char in 'A'..'Z') { + upperCase = true + continue + } + + if (char in '0'..'9') { + numbers = true + continue + } + + if (char == '-') { + dash = true + continue + } + + specialChars += char + } + } + + var regexClass = "" + + if (lowerCase) { + regexClass += "a-z" + } + + if (upperCase) { + regexClass += "A-Z" + } + + if (numbers) { + regexClass += "0-9" + } + + regexClass += specialChars + + if (dash) { + regexClass += "-" + } + + syntaticMatch = ("[" + regexClass + "]{1,$maxLength}").toRegex() + } + + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + for (choice in choices) { + if (value.substring(offset).startsWith(choice)) { + return MatchResult(listOf(choice), offset + choice.length, listOf(this), offset + choice.length) + } + } + + val matchResult = syntaticMatch.matchAt(value, offset) ?: return NoMatch + + return MatchResult(listOf(matchResult.value), offset + matchResult.value.length, listOf(this), offset + matchResult.value.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + for (choice in choices) { + if (value.substring(offset).startsWith(choice)) { + return MatchResult(listOf(choice), offset + choice.length, listOf(this), offset + choice.length) + } + } + return NoMatch.copy(longestMatch = offset) + } + + +} 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 new file mode 100644 index 00000000..a3874f30 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/GrammarOptionValue.kt @@ -0,0 +1,122 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +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.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.SemanticDataRepository +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.OptionValueInformation + +open class GrammarOptionValue( + override val validatorName: String, + val combinator: Combinator + +) : OptionValueInformation { + + override fun getAutoCompleteOptions(project: Project): Set { + return emptySet() + } + + override fun getErrorMessage(value: String): String? { + throw IllegalStateException("This should not be called") + } + + /** + * Generates problem descriptors based on the value. + * + * @param property - the Psi Element we are examining. + * @param holder - A problem holder that we should add to. + */ + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + val value = property.valueText ?: return + + val syntaticMatch = combinator.SyntacticMatch(value, 0) + + try { + + if (syntaticMatch.matchResult == -1) { + + // We couldn't match the syntax, and we don't have tokens + // If we matched up to a specific point, we can highlight the rest. + // If we matched to the end, it's unclear what to highlight (sometimes it can be the last char, maybe the whole thing), so we will match everything. + val tr = if (syntaticMatch.longestMatch < value.length) { + TextRange(syntaticMatch.longestMatch, value.length) + } else { + TextRange(0, value.length) + } + + holder.registerProblem(property.valueNode.psi, "${property.key}'s value does not match the expected format. Possible reasons include unrecognized characters or premature end of input.", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, tr) + + + return + } + + val semanticMatch = combinator.SemanticMatch(value, 0) + + if (semanticMatch.matchResult == -1) { + + if (semanticMatch.tokens.size < syntaticMatch.tokens.size) { + // We couldn't fully understand everything, but syntactically recognized, let's highlight the first token as the problem. + + + // Get the token from the semanticMatch tokens, where the match.longestMatch points to the token + + var tokenLength = 0 + var tokenIndex = 0 + for (tokens in syntaticMatch.tokens) { + tokenLength += tokens.length + if (tokenLength > semanticMatch.longestMatch) { + break + } + tokenIndex++ + } + + val problemToken = syntaticMatch.tokens[tokenIndex] + + val problemTerminal = syntaticMatch.terminals[tokenIndex] + + + val prefixLength = semanticMatch.longestMatch + + val tr = TextRange(prefixLength, prefixLength + problemToken.length) + + val quickFixes = mutableListOf() + + if (problemTerminal is LiteralChoiceTerminal) { + for (choice in problemTerminal.choices) { + quickFixes.add(ReplaceInvalidLiteralChoiceQuickFix(prefixLength, problemToken, choice)) + } + } else if (problemTerminal is FlexibleLiteralChoiceTerminal) { + for (choice in problemTerminal.choices) { + quickFixes.add(ReplaceInvalidLiteralChoiceQuickFix(prefixLength, problemToken, choice)) + } + } + + holder.registerProblem(property.valueNode.psi, "${property.key}'s value is correctly format but seems invalid.", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, tr, *quickFixes.toTypedArray()) + } else { + holder.registerProblem(property.valueNode.psi, "${property.key}'s value is correctly format but seems invalid.", ProblemHighlightType.GENERIC_ERROR_OR_WARNING) + } + + + + return + } + + } catch (e: RuntimeException) { + LOG.error("Error while processing ${property.key} with value ${value}", e) + return holder.registerProblem(property.valueNode.psi, "Internal error, please report an bug to the systemd plugin. Include the Key and Value used.", ProblemHighlightType.ERROR) + } + + return + + + } + + companion object { + private val LOG = Logger.getInstance(SemanticDataRepository::class.java) + } +} 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 new file mode 100644 index 00000000..a9b3e6e6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt @@ -0,0 +1,25 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +class IntegerTerminal(private val minInclusive: Int,private val maxExclusive: Int) : TerminalCombinator { + + val intRegex = "-?[0-9]+".toRegex() + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val matchResult = intRegex.matchAt(value, offset) ?: return NoMatch + + return MatchResult(listOf(matchResult.value), offset + matchResult.value.length, listOf(this), offset + matchResult.value.length) + + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val matchResult = intRegex.matchAt(value, offset) ?: return NoMatch + + val intValue = matchResult.value.toInt() + + if (intValue < minInclusive || intValue >= maxExclusive) { + return NoMatch.copy(longestMatch = offset) + } + + return MatchResult(listOf(matchResult.value), offset + matchResult.value.length, listOf(this), offset + matchResult.value.length) + } +} 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 new file mode 100644 index 00000000..08ee0d2a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +class LiteralChoiceTerminal(vararg var choices: String) : TerminalCombinator { + + init { + choices.sortBy { -it.length } + } + + private fun match(value: String, offset: Int): MatchResult { + for (choice in choices) { + if (value.substring(offset).startsWith(choice)) { + return MatchResult(listOf(choice), offset + choice.length, listOf(this), offset + choice.length) + } + } + return NoMatch + } + + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + return match(value, offset) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + return match(value, offset) + } +} 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 new file mode 100644 index 00000000..c0a51a32 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/MatchResult.kt @@ -0,0 +1,11 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +data class MatchResult(val tokens: List, val matchResult: Int, val terminals: List, val longestMatch : Int ) { + init { + if (tokens.size != terminals.size) { + throw IllegalArgumentException("Tokens and terminals must be the same size, ${tokens.size} != ${terminals.size}") + } + } +} + +val NoMatch = MatchResult(emptyList(), -1, emptyList(), 0) 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 new file mode 100644 index 00000000..476dfb82 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import kotlin.math.max + +/** + * One Or More Combinator + */ +class OneOrMore(val combinator : Combinator) : Combinator { + + private fun match(value: String, offset: Int, f: (String, Int) -> MatchResult): MatchResult { + var index = offset + var match = f(value, index) + + val tokens = mutableListOf() + val terminals = mutableListOf() + + if (match.matchResult == -1) { + return match + } + + var maxLength = 0 + while (match.matchResult != -1) { + index = match.matchResult + tokens.addAll(match.tokens) + terminals.addAll(match.terminals) + + match = f(value, index) + + maxLength = max(maxLength, match.longestMatch) + } + + 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) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/RegexTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/RegexTerminal.kt new file mode 100644 index 00000000..9e898c85 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/RegexTerminal.kt @@ -0,0 +1,20 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +class RegexTerminal(syntaticMatchStr : String, semanticMatchStr: String ) : TerminalCombinator { + + val syntaticMatch = syntaticMatchStr.toRegex() + val semanticMatch = semanticMatchStr.toRegex() + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val matchResult = syntaticMatch.matchAt(value, offset) ?: return NoMatch + + return MatchResult(listOf(matchResult.value), offset + matchResult.value.length, listOf(this), offset + matchResult.value.length) + + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val matchResult = semanticMatch.matchAt(value, offset) ?: return NoMatch + + return MatchResult(listOf(matchResult.value), offset + matchResult.value.length, listOf(this), offset + matchResult.value.length) + } +} 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 new file mode 100644 index 00000000..7e0e40a6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import kotlin.math.max + +/** + * This is a sequence of tokens that must match all of them. + */ +class SequenceCombinator(vararg val tokens: Combinator) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + var index = offset + val resultTokens = mutableListOf() + val resultTerminals = mutableListOf() + var maxLength = 0 + + for (token in tokens) { + val match = token.SyntacticMatch(value, index) + + resultTokens.addAll(match.tokens) + resultTerminals.addAll(match.terminals) + maxLength = max(maxLength, match.longestMatch) + if (match.matchResult == -1) { + // No forward progress + return MatchResult(resultTokens, -1, resultTerminals, maxLength) + } + + index = match.matchResult + + + } + return MatchResult(resultTokens, index, resultTerminals, maxLength) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + var index = offset + + val resultTokens = mutableListOf() + val resultTerminals = mutableListOf() + var maxLength = 0 + + for (token in tokens) { + val match = token.SemanticMatch(value, index) + + resultTokens.addAll(match.tokens) + resultTerminals.addAll(match.terminals) + maxLength = max(maxLength, match.longestMatch) + + if (match.matchResult == -1) { + // No forward progress + return MatchResult(resultTokens, -1, resultTerminals, maxLength) + } + index = match.matchResult + + } + return MatchResult(resultTokens, index, resultTerminals, maxLength) + } +} 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 new file mode 100644 index 00000000..8e01cf76 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt @@ -0,0 +1,3 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +interface TerminalCombinator : Combinator 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 new file mode 100644 index 00000000..f935ea48 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import kotlin.math.max + +/** + * Zero Or More Combinator + */ +class ZeroOrMore(val combinator : Combinator) : Combinator { + + 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) + + + if (match.matchResult == -1) { + return MatchResult(tokens, offset, terminals, match.longestMatch) + } + + var maxLength = match.longestMatch + + + while (match.matchResult != -1) { + index = match.matchResult + tokens.addAll(match.tokens) + terminals.addAll(match.terminals) + + match = f(value, index) + maxLength = max(maxLength, match.longestMatch) + } + + 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) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/AbstractUnitFileTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/AbstractUnitFileTest.kt index 85171427..ad4ad0b9 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/AbstractUnitFileTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/AbstractUnitFileTest.kt @@ -1,6 +1,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInspection.LocalInspectionTool import com.intellij.psi.PsiElement @@ -8,8 +9,11 @@ import com.intellij.psi.PsiFile import com.intellij.psi.tree.IElementType import com.intellij.psi.util.PsiTreeUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.utils.module.assertContains import junit.framework.TestCase import net.sjrx.intellij.plugins.systemdunitfiles.generated.UnitFileElementTypeHolder +import org.hamcrest.CoreMatchers.hasItem +import org.hamcrest.MatcherAssert.assertThat import java.util.* import java.util.stream.Collectors @@ -61,5 +65,17 @@ abstract class AbstractUnitFileTest : BasePlatformTestCase() { protected fun assertStringContains(subject: String, value: String) { TestCase.assertTrue("Expected that $value contains $subject", value.contains(subject)) } + + @JvmStatic + protected fun assertContainsQuickfix(info: HighlightInfo, quickfixName: String) { + + var found = false + val quickFixes = info.quickFixActionRanges.map { + it -> + it.first.action.text + } + + assertThat(quickFixes, hasItem(quickfixName)) + } } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForCPUShares.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForCPUShares.kt new file mode 100644 index 00000000..a8280b72 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForCPUShares.kt @@ -0,0 +1,109 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import junit.framework.TestCase +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InvalidValidInspectionForCPUShares : AbstractUnitFileTest() { + + + fun testNoWarningWhenTwoSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUShares=2 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testNoWarningWhenMaxValueSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUShares=262144 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWeakWarningWhenOneIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUShares=1 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("CPUShares's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("1", info.text) + } + + fun testWeakWarningWhenNegativeTenIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUShares=-10 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("CPUShares's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("-10", info.text) + } + + + fun testWeakWarningWhenValueTooBigIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUShares=262145 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("CPUShares's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("262145", info.text) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForCPUWeight.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForCPUWeight.kt new file mode 100644 index 00000000..0bee01c3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForCPUWeight.kt @@ -0,0 +1,126 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import junit.framework.TestCase +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InvalidValidInspectionForCPUWeight : AbstractUnitFileTest() { + + fun testNoWarningWhenIdleSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUWeight=idle + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testNoWarningWhenOneSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUWeight=1 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testNoWarningWhenTenThousandSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUWeight=10000 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWeakWarningWhenZeroIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUWeight=0 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("CPUWeight's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("0", info.text) + } + + fun testWeakWarningWhenNegativeTenIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUWeight=-10 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("CPUWeight's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("-10", info.text) + } + + + fun testWeakWarningWhenNegativeHundredThousandIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + CPUWeight=-100000 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("CPUWeight's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("-100000", info.text) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForIOReadBandwidthMax.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForIOReadBandwidthMax.kt new file mode 100644 index 00000000..6fcf4fd7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForIOReadBandwidthMax.kt @@ -0,0 +1,110 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import junit.framework.TestCase +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InvalidValidInspectionForIOReadBandwidthMax : AbstractUnitFileTest() { + + fun testNoWarningWhenNumberSpecifiedWithoutUnit() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + IOReadBandwidthMax=/home 2 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + + } + + fun testNoWarningWhenNumberSpecifiedWithUnit() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + IOReadBandwidthMax=/home 2M + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + + } + + fun testWeakWarningWhenNegativeIntegerSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + IOReadBandwidthMax=-5 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("IOReadBandwidthMax's value does not match the expected format. Possible reasons include unrecognized characters or premature end of input.", info!!.description) + TestCase.assertEquals("-5", info.text) + } + + fun testWeakWarningWhenDeviceSpecifiedWithNoValue() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + IOReadBandwidthMax=/home + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("IOReadBandwidthMax's value does not match the expected format. Possible reasons include unrecognized characters or premature end of input.", info!!.description) + TestCase.assertEquals("/home", info.text) + } + + + fun testWeakWarningWhenPositiveIntegerSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + IOReadBandwidthMax=5 + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("IOReadBandwidthMax's value does not match the expected format. Possible reasons include unrecognized characters or premature end of input.", info!!.description) + TestCase.assertEquals("5", info.text) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForImagePolicy.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForImagePolicy.kt new file mode 100644 index 00000000..4227cdf8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValidInspectionForImagePolicy.kt @@ -0,0 +1,136 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import junit.framework.TestCase +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InvalidValidInspectionForImagePolicy : AbstractUnitFileTest() { + + fun testNoWarningWhenNumberSpecifiedWithoutUnit() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + RootImagePolicy=root=unprotected + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + + } + + fun testWeakWarningWhenInvalidPartitionTypeIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + RootImagePolicy=opt=unprotected + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("RootImagePolicy's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("opt", info.text) + } + + fun testWeakWarningWhenInvalidPolicyFlagIsSpecified() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + RootImagePolicy=root=unsigned + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("RootImagePolicy's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("unsigned", info.text) + } + + fun testWeakWarningWhenInvalidPolicyFlagIsSpecifiedInSecondaryPolicy() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + RootImagePolicy=home=encrypted+used + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + + // Verification + assertSize(1, highlights) + val info = highlights[0] + + AbstractUnitFileTest.Companion.assertStringContains("RootImagePolicy's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("used", info.text) + assertContainsQuickfix(info, "Replace 'used' with 'absent'") + } + + fun testWeakWarningWhenIncompletePolicyFlagIsSpecifiedInSecondaryPolicy() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + RootImagePolicy=home=encrypted+absent+ + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("RootImagePolicy's value does not match the expected format. Possible reasons include unrecognized characters or premature end of input", info!!.description) + TestCase.assertEquals("home=encrypted+absent+", info.text) + } + + fun testWeakWarningWhenInvalidPolicySetInSecondPolicy() { + // Fixture Setup + // language="unit file (systemd)" + val file = """ + [Service] + RootImagePolicy=home=encrypted:root=encrypted+used + """.trimIndent() + + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + val info = highlights[0] + AbstractUnitFileTest.Companion.assertStringContains("RootImagePolicy's value is correctly format but seems invalid", info!!.description) + TestCase.assertEquals("used", info.text) + assertContainsQuickfix(info, "Replace 'used' with 'absent'") + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueTest.kt index 8de7959a..bdb97a0c 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/OptionValueTest.kt @@ -34,8 +34,8 @@ class OptionValueTest : AbstractUnitFileTest() { println("Missing:$totalMissingValidators") println("Found:$totalFoundValidators") - if (totalMissingValidators > 600) { - assertEquals(sortedList, "") + if (totalMissingValidators > 560) { + assertEquals("Number of missing validators is too high at ${totalMissingValidators} vs. found ${totalFoundValidators}", sortedList, "") } } 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 new file mode 100644 index 00000000..4ae0361e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt @@ -0,0 +1,663 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import junit.framework.TestCase + +class GrammarTest : TestCase() { + + fun TerminalType(o: TerminalCombinator): String { + return o.javaClass.simpleName + } + + fun TerminalTypes(os: List): List { + return os.map { o -> TerminalType(o) } + } + + fun testRegexTerminalMatches() { + /** + * Fixture Setup + */ + + // This regex should match something that looks like a byte specifier possibly negative, syntactically. + // But semantically only accept positive values and known units. + + val regexTerminal = RegexTerminal("-?[0-9]+\\s*[A-Z]", "[0-9]*[1-9]\\s*[BKMG]") + + val semValid = "1K" + val synValid = "-2Z" + val invalid = "6,000 People" + + val garbage = "XX" + val semValidFromOffset = "${garbage}${semValid}" + val synValidFromOffset = "${garbage}${synValid}" + val invalidFromOffset = "${garbage}${invalid}" + + + /** + * Execute SUT & Verification + */ + var match = regexTerminal.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("1K"), match.tokens) + assertEquals(listOf("RegexTerminal"), TerminalTypes(match.terminals)) + + match = regexTerminal.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("1K"), match.tokens) + assertEquals(listOf("RegexTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, regexTerminal.SemanticMatch(synValid, 0)) + + match = regexTerminal.SyntacticMatch(synValid, 0) + assertEquals(synValid.length, match.matchResult) + assertEquals(listOf("-2Z"), match.tokens) + assertEquals(listOf("RegexTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, regexTerminal.SemanticMatch(invalid, 0)) + assertEquals(NoMatch, regexTerminal.SyntacticMatch(invalid, 0)) + + match = regexTerminal.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("1K"), match.tokens) + assertEquals(listOf("RegexTerminal"), TerminalTypes(match.terminals)) + + match = regexTerminal.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("1K"), match.tokens) + assertEquals(listOf("RegexTerminal"), TerminalTypes(match.terminals)) + + match = regexTerminal.SyntacticMatch(synValidFromOffset, garbage.length) + assertEquals(synValidFromOffset.length, match.matchResult) + assertEquals(listOf("-2Z"), match.tokens) + assertEquals(listOf("RegexTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, regexTerminal.SemanticMatch(synValidFromOffset, garbage.length)) + assertEquals(NoMatch, regexTerminal.SyntacticMatch(invalidFromOffset, garbage.length)) + assertEquals(NoMatch, regexTerminal.SemanticMatch(invalidFromOffset, garbage.length)) + + + } + + + fun testLiteralChoiceTerminalMatches() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + + val literalChoiceTerminal = LiteralChoiceTerminal( "foo", "bar", "baz") + + val semValid = "foo" + val garbage = "XX" + val invalid = "qux" + + val semValidFromOffset="${garbage}${semValid}" + val invalidFromOffset = "${garbage}${invalid}" + val invalidFromOffsetWithSemValidPrefix = "${semValid}${invalid}" + + /** + * Execute SUT & Verification + */ + var match = literalChoiceTerminal.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, literalChoiceTerminal.SemanticMatch(invalid, 0)) + assertEquals(NoMatch, literalChoiceTerminal.SyntacticMatch(invalid, 0)) + + match = literalChoiceTerminal.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, literalChoiceTerminal.SemanticMatch(invalidFromOffset, garbage.length)) + assertEquals(NoMatch, literalChoiceTerminal.SyntacticMatch(invalidFromOffset, garbage.length)) + + assertEquals(NoMatch, literalChoiceTerminal.SemanticMatch(invalidFromOffsetWithSemValidPrefix, semValid.length)) + assertEquals(NoMatch, literalChoiceTerminal.SyntacticMatch(invalidFromOffsetWithSemValidPrefix, semValid.length)) + + } + + fun testLiteralChoiceTerminalMatchesLongest() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + + val literalChoiceTerminal = LiteralChoiceTerminal( "a", "ab", "abc") + + val semValid = "abc" + val garbage = "XX" + + val semValidFromOffset="${garbage}${semValid}" + + /** + * Execute SUT & Verfication + */ + var match = literalChoiceTerminal.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("abc"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("abc"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + } + + fun testFlexibleLiteralChoiceTerminalMatches() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + + val literalChoiceTerminal = FlexibleLiteralChoiceTerminal( "foo", "bar", "baz") + + val semValid = "foo" + val garbage = "XX" + val invalid = "qux" + + val semValidFromOffset="${garbage}${semValid}" + val invalidFromOffset = "${garbage}${invalid}" + val invalidFromOffsetWithSemValidPrefix = "${semValid}${invalid}" + + /** + * Execute SUT & Verification + */ + var match = literalChoiceTerminal.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, literalChoiceTerminal.SemanticMatch(invalid, 0)) + + match = literalChoiceTerminal.SyntacticMatch(invalid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("qux"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("foo"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SemanticMatch(invalidFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + assertEquals(listOf(), match.tokens) + assertEquals(listOf(), TerminalTypes(match.terminals)) + assertEquals(2, match.longestMatch) + + match = literalChoiceTerminal.SyntacticMatch(invalidFromOffset, garbage.length) + assertEquals(5, match.matchResult) + assertEquals(listOf("qux"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + assertEquals(5, match.longestMatch) + + + + match = literalChoiceTerminal.SemanticMatch(invalidFromOffsetWithSemValidPrefix, semValid.length) + assertEquals(-1, match.matchResult) + assertEquals(listOf(), match.tokens) + assertEquals(listOf(), TerminalTypes(match.terminals)) + assertEquals(3, match.longestMatch) + + match = literalChoiceTerminal.SyntacticMatch(invalidFromOffsetWithSemValidPrefix, semValid.length) + assertEquals(6, match.matchResult) + assertEquals(listOf("qux"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + assertEquals(6, match.longestMatch) + + } + + fun testFlexibleLiteralChoiceTerminalMatchesLongest() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + + val literalChoiceTerminal = FlexibleLiteralChoiceTerminal( "a", "ab", "abc") + + val semValid = "abc" + val garbage = "XX" + + val semValidFromOffset="${garbage}${semValid}" + + /** + * Execute SUT & Verfication + */ + var match = literalChoiceTerminal.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("abc"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("abc"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + } + + fun testFlexibleLiteralChoiceTerminalMatchesSemanticallyFirstLongest() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + + val literalChoiceTerminal = FlexibleLiteralChoiceTerminal( "abc", "defg") + + val semValid = "abc" + val garbage = "xx" + + val semValidAndGarbage = "${semValid}${garbage}" + + + /** + * Execute SUT & Verfication + */ + var match = literalChoiceTerminal.SemanticMatch(semValidAndGarbage, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("abc"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = literalChoiceTerminal.SyntacticMatch(semValidAndGarbage, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("abc"), match.tokens) + assertEquals(listOf("FlexibleLiteralChoiceTerminal"), TerminalTypes(match.terminals)) + } + + + + + fun testSequenceCombinatorMatches() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + val number = RegexTerminal("-?[0-9]+", "[0-9]*[1-9]") + val unit = LiteralChoiceTerminal("B", "K", "M", "G") + val sequenceCombinator = SequenceCombinator(number, unit) + + val semValid = "1K" + val synValid = "-0B" + val semPrefix = "1" + val synPrefix = "0" + val invalid = "Hello World" + + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val synValidFromOffset = "${garbage}${synValid}" + + val semPrefixFromOffset = "${garbage}${semPrefix}" + val synPrefixFromOffset = "${garbage}${synPrefix}" + val invalidFromOffset = "${garbage}${invalid}" + + /** + * Execute SUT & Verification + */ + var match = sequenceCombinator.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("1", "K"), match.tokens) + assertEquals(listOf("RegexTerminal","LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = sequenceCombinator.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("1", "K"), match.tokens) + assertEquals(listOf("RegexTerminal","LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = sequenceCombinator.SyntacticMatch(synValid, 0) + assertEquals(synValid.length, match.matchResult) + assertEquals(listOf("-0", "B"), match.tokens) + assertEquals(listOf("RegexTerminal","LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + + assertEquals(NoMatch, sequenceCombinator.SemanticMatch(synValid, 0)) + + match = sequenceCombinator.SyntacticMatch(semPrefix, 0) + assertEquals(-1, match.matchResult) + + match = sequenceCombinator.SemanticMatch(semPrefix, 0) + assertEquals(-1, match.matchResult) + + match = sequenceCombinator.SyntacticMatch(synPrefix, 0) + assertEquals(-1, match.matchResult) + + match = sequenceCombinator.SemanticMatch(synPrefix, 0) + assertEquals(-1, match.matchResult) + + match = sequenceCombinator.SyntacticMatch(invalid, 0) + assertEquals(-1, match.matchResult) + + match = sequenceCombinator.SemanticMatch(invalid, 0) + assertEquals(-1, match.matchResult) + + match = sequenceCombinator.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("1", "K"), match.tokens) + assertEquals(listOf("RegexTerminal","LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = sequenceCombinator.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("1", "K"), match.tokens) + assertEquals(listOf("RegexTerminal","LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = sequenceCombinator.SyntacticMatch(synValidFromOffset, garbage.length) + assertEquals(synValidFromOffset.length, match.matchResult) + assertEquals(listOf("-0", "B"), match.tokens) + assertEquals(listOf("RegexTerminal","LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, sequenceCombinator.SemanticMatch(synValidFromOffset, garbage.length)) + match = sequenceCombinator.SemanticMatch(semPrefixFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + + match = sequenceCombinator.SyntacticMatch(semPrefixFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + assertEquals(NoMatch, sequenceCombinator.SemanticMatch(synPrefixFromOffset, garbage.length)) + + match = sequenceCombinator.SyntacticMatch(synPrefixFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + + assertEquals(NoMatch, sequenceCombinator.SemanticMatch(invalidFromOffset, garbage.length)) + assertEquals(NoMatch, sequenceCombinator.SyntacticMatch(invalidFromOffset, garbage.length)) + } + + + fun testAlternativeCombinatorMatches() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + + val on = LiteralChoiceTerminal("on") + val off = LiteralChoiceTerminal("off") + + val alternativeCombinator = AlternativeCombinator(on, off) + + val semValid = "on" + val semValid2 = "off" + val invalid = "bleh" + + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val semValid2FromOffset = "${garbage}${semValid2}" + val invalidFromOffset = "${garbage}${invalid}" + + /** + * Execute SUT & Verification + */ + var match = alternativeCombinator.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("on"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = alternativeCombinator.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("on"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = alternativeCombinator.SyntacticMatch(semValid2, 0) + assertEquals(semValid2.length, match.matchResult) + assertEquals(listOf("off"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = alternativeCombinator.SemanticMatch(semValid2, 0) + assertEquals(semValid2.length, match.matchResult) + assertEquals(listOf("off"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, alternativeCombinator.SyntacticMatch(invalid, 0)) + assertEquals(NoMatch, alternativeCombinator.SemanticMatch(invalid, 0)) + + match = alternativeCombinator.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("on"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = alternativeCombinator.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("on"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = alternativeCombinator.SemanticMatch(semValid2FromOffset, garbage.length) + assertEquals(semValid2FromOffset.length, match.matchResult) + assertEquals(listOf("off"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + match = alternativeCombinator.SyntacticMatch(semValid2FromOffset, garbage.length) + assertEquals(semValid2FromOffset.length, match.matchResult) + assertEquals(listOf("off"), match.tokens) + assertEquals(listOf("LiteralChoiceTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, alternativeCombinator.SemanticMatch(invalidFromOffset, garbage.length)) + assertEquals(NoMatch, alternativeCombinator.SyntacticMatch(invalidFromOffset, garbage.length)) + } + + fun testOneOrMoreCombinatorMatches() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + val fizzOrBuzz = RegexTerminal("[a-z]{4}", "fizz|buzz") + + val oneOrMoreCombinator = OneOrMore(fizzOrBuzz) + + val semValid = "fizzbuzzfizz" + val synValid = "blehblehbleh" + val invalid = "Hello World" + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val synValidFromOffset = "${garbage}${synValid}" + val invalidFromOffset = "${garbage}${invalid}" + + /** + * Execute SUT & Verification + */ + var match = oneOrMoreCombinator.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = oneOrMoreCombinator.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = oneOrMoreCombinator.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, oneOrMoreCombinator.SemanticMatch(synValid, 0)) + + assertEquals(NoMatch, oneOrMoreCombinator.SyntacticMatch(invalid, 0)) + assertEquals(NoMatch, oneOrMoreCombinator.SemanticMatch(invalid, 0)) + + match = oneOrMoreCombinator.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 = oneOrMoreCombinator.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 = oneOrMoreCombinator.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, oneOrMoreCombinator.SemanticMatch(synValidFromOffset, garbage.length)) + + assertEquals(NoMatch, oneOrMoreCombinator.SyntacticMatch(invalidFromOffset, garbage.length)) + assertEquals(NoMatch, oneOrMoreCombinator.SemanticMatch(invalidFromOffset, garbage.length)) + + } + + fun testZeroOrMoreCombinatorMatches() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + val fizzOrBuzz = RegexTerminal("[a-z]{4}", "fizz|buzz") + + val zeroOrMoreCombinator = ZeroOrMore(fizzOrBuzz) + + val semValid = "fizzbuzzfizz" + val synValid = "blehblehbleh" + val semValidEmpty = "" + val invalid = "Hello World" + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val semValidEmptyFromOffset = "${garbage}${semValidEmpty}" + val synValidFromOffset = "${garbage}${synValid}" + val invalidFromOffset = "${garbage}${invalid}" + + /** + * Execute SUT & Verification + */ + var match = zeroOrMoreCombinator.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = zeroOrMoreCombinator.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = zeroOrMoreCombinator.SyntacticMatch(synValid, 0) + assertEquals(synValid.length, match.matchResult) + assertEquals(listOf("bleh", "bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = zeroOrMoreCombinator.SemanticMatch(synValid, 0) + assertEquals(0, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SyntacticMatch(semValidEmpty, 0) + assertEquals(semValidEmpty.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SemanticMatch(semValidEmpty, 0) + assertEquals(semValidEmpty.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SyntacticMatch(invalid, 0) + assertEquals(0, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SemanticMatch(invalid, 0) + assertEquals(0, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.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 = zeroOrMoreCombinator.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 = zeroOrMoreCombinator.SyntacticMatch(synValidFromOffset, garbage.length) + assertEquals(synValidFromOffset.length, match.matchResult) + assertEquals(listOf("bleh", "bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = zeroOrMoreCombinator.SemanticMatch(synValidFromOffset, garbage.length) + assertEquals(garbage.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SyntacticMatch(semValidEmptyFromOffset, garbage.length) + assertEquals(garbage.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SemanticMatch(semValidEmptyFromOffset, garbage.length) + assertEquals(garbage.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SyntacticMatch(invalidFromOffset, garbage.length) + assertEquals(garbage.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = zeroOrMoreCombinator.SemanticMatch(invalidFromOffset, garbage.length) + assertEquals(garbage.length, match.matchResult) + assertEquals(listOf(), match.tokens) + } + + fun testEOFCombinatorMatches() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + + val eof = EOF() + + val semValid = "" + val invalid = "Hello World" + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val invalidFromOffset = "${garbage}${invalid}" + + /** + * Execute SUT & Verification + */ + var match = eof.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = eof.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + assertEquals(NoMatch, eof.SyntacticMatch(invalid, 0)) + assertEquals(NoMatch, eof.SemanticMatch(invalid, 0)) + + match = eof.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + match = eof.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf(), match.tokens) + + assertEquals(NoMatch, eof.SyntacticMatch(invalidFromOffset, garbage.length)) + assertEquals(NoMatch, eof.SemanticMatch(invalidFromOffset, garbage.length)) + } + +}