Skip to content

Commit 141df26

Browse files
author
Steve Ramage
committed
feat: Add support for Token Based validators
1 parent 753e26d commit 141df26

File tree

6 files changed

+724
-2
lines changed

6 files changed

+724
-2
lines changed

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import com.intellij.openapi.util.NotNullLazyValue
1212
import com.intellij.openapi.util.text.StringUtil
1313
import com.intellij.util.ObjectUtils
1414
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.*
15+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AllGrammarOptions
16+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue
17+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IOLimitOptionValue
1518
import org.apache.commons.io.IOUtils
1619
import java.io.BufferedReader
1720
import java.io.IOException
@@ -87,6 +90,8 @@ class SemanticDataRepository private constructor() {
8790
validatorMap.putAll(AllowedCpuSetOptionValue.validators)
8891
validatorMap.putAll(TtySizeOptionValue.validators)
8992
validatorMap.putAll(ExecDirectoriesOptionValue.validators)
93+
validatorMap.putAll(IOLimitOptionValue.validators)
94+
validatorMap.putAll(AllGrammarOptions.validators)
9095

9196
// Scopes are not supported since they aren't standard unit files.
9297

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar
2+
3+
import com.intellij.openapi.project.Project
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator
5+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.OptionValueInformation
6+
7+
interface Combinator {
8+
9+
// TODO add longest prefix match
10+
// Note about coloring on this.
11+
12+
/**
13+
* WARNING: At the current time this combinator implementation doesn't necessarily guarantee a match.
14+
*
15+
* Seq(ZeroOrMore(Literal("fizz")), Literal("fizz"))
16+
*
17+
* If you try and match "fizz", the ZeroOrMore would greedily consume the fizz, and the second wouldn't match.
18+
*
19+
* I'm unclear if this will actually be a problem, and whether it's worth fixing.
20+
*/
21+
22+
/**
23+
* This checks the value string, starting at offset for a syntactic match.
24+
*
25+
* In a nutshell a syntactic match might accept things that we should color and try and analyze
26+
* but might be incorrect.
27+
*
28+
* For example if you something accepts a positive number, a syntactic regex should match any number even negative or floats
29+
*
30+
* The return value is -1 for no match, or a new offset if this token matched something.
31+
*/
32+
fun SyntacticMatch(value : String, offset: Int): Int
33+
34+
/**
35+
* This checks the value string, starting at offset for a semantic match.
36+
*
37+
* In a nutshell a semantic match means we understood and it valid as far as the grammar is concerned.
38+
*
39+
* The return value is -1 for no match, or a new offset if this token matched something.
40+
*/
41+
fun SemanticMatch(value : String, offset: Int): Int
42+
43+
44+
}
45+
46+
47+
48+
class RegexTerminal(val syntaticMatchStr : String, val semanticMatchStr: String ) : Combinator {
49+
50+
val syntaticMatch = syntaticMatchStr.toRegex()
51+
val semanticMatch = semanticMatchStr.toRegex()
52+
53+
override fun SyntacticMatch(value: String, offset: Int): Int {
54+
val matchResult = syntaticMatch.matchAt(value, offset) ?: return -1
55+
56+
return offset + matchResult.value.length
57+
58+
}
59+
60+
override fun SemanticMatch(value: String, offset: Int): Int {
61+
val matchResult = semanticMatch.matchAt(value, offset) ?: return -1
62+
63+
return offset + matchResult.value.length
64+
}
65+
}
66+
67+
val BYTES = RegexTerminal("[0-9]+[a-zA-Z]*\\s*", "[0-9]+[KMGT]?\\s*")
68+
val DEVICE = RegexTerminal("\\S+\\s*", "/[^\\u0000.]+\\s*")
69+
val IOPS = RegexTerminal("[0-9]+[a-zA-Z]*\\s*", "[0-9]+[KMGT]?\\s*")
70+
71+
class LiteralChoiceTerminal(vararg val choices: String) : Combinator {
72+
override fun SyntacticMatch(value: String, offset: Int): Int {
73+
for (choice in choices) {
74+
if (value.substring(offset).startsWith(choice)) {
75+
return offset + choice.length
76+
}
77+
}
78+
return -1
79+
}
80+
81+
override fun SemanticMatch(value: String, offset: Int): Int {
82+
for (choice in choices) {
83+
if (value.substring(offset).startsWith(choice)) {
84+
return offset + choice.length
85+
}
86+
}
87+
return -1
88+
}
89+
}
90+
91+
// Image Polcies
92+
// https://www.freedesktop.org/software/systemd/man/latest/systemd.image-policy.html#
93+
val IMAGE_POLICY_SEPARATOR = LiteralChoiceTerminal(":")
94+
val PARTITION_IDENTIFIER = LiteralChoiceTerminal("root", "usr", "home", "srv", "esp", "xbootldr", "swap", "root-verity", "root-verity-sig", "usr-verity", "usr-verity-sig", "tmp", "var")
95+
val PARTITION_POLICY_FLAG = LiteralChoiceTerminal("unprotected", "verity", "signed", "encrypted", "unused", "absent", "read-only-off", "read-only-on", "growfs-off", "growfs-on")
96+
val PARTITION_POLICY_FLAG_SEPARATOR = LiteralChoiceTerminal("+")
97+
val PARTITION_IDENTIFIER_FLAG_SEPARATOR = LiteralChoiceTerminal("=")
98+
99+
100+
val PARTITION_POLICY_FLAGS = SequenceCombinator(PARTITION_POLICY_FLAG, ZeroOrMore(SequenceCombinator(PARTITION_POLICY_FLAG_SEPARATOR, PARTITION_POLICY_FLAG)))
101+
val SINGLE_EXPLICIT_IMAGE_POLICY = SequenceCombinator(PARTITION_IDENTIFIER, PARTITION_IDENTIFIER_FLAG_SEPARATOR, PARTITION_POLICY_FLAGS)
102+
val DEFAULT_IMAGE_POLICY = SequenceCombinator(PARTITION_POLICY_FLAG_SEPARATOR, PARTITION_POLICY_FLAGS)
103+
var IMAGE_POLICY=AlternativeCombinator(SINGLE_EXPLICIT_IMAGE_POLICY, DEFAULT_IMAGE_POLICY)
104+
val IMAGE_POLICY_COMBINATOR = SequenceCombinator(IMAGE_POLICY, ZeroOrMore(SequenceCombinator(IMAGE_POLICY_SEPARATOR, IMAGE_POLICY)), EOF())
105+
106+
107+
/**
108+
* This is a sequence of tokens that must match all of them.
109+
*/
110+
class SequenceCombinator(vararg val tokens: Combinator) : Combinator{
111+
override fun SyntacticMatch(value: String, offset: Int): Int {
112+
var index = offset
113+
for (token in tokens) {
114+
val match = token.SyntacticMatch(value, index)
115+
if (match == -1) {
116+
// No forward progress
117+
return -1
118+
}
119+
index = match
120+
}
121+
return index
122+
}
123+
124+
override fun SemanticMatch(value: String, offset: Int): Int {
125+
var index = offset
126+
for (token in tokens) {
127+
val match = token.SemanticMatch(value, index)
128+
if (match == -1) {
129+
// No forward progress
130+
return -1
131+
}
132+
index = match
133+
}
134+
return index
135+
}
136+
}
137+
138+
139+
/**
140+
* This is a sequence of tokens that must match any of them.
141+
*/
142+
class AlternativeCombinator(vararg val tokens: Combinator) : Combinator {
143+
override fun SyntacticMatch(value: String, offset: Int): Int {
144+
for (token in tokens) {
145+
val match = token.SyntacticMatch(value, offset)
146+
if (match != -1) {
147+
return match
148+
}
149+
}
150+
return -1
151+
}
152+
153+
override fun SemanticMatch(value: String, offset: Int): Int {
154+
for (token in tokens) {
155+
val match = token.SemanticMatch(value, offset)
156+
if (match != -1) {
157+
return match
158+
}
159+
}
160+
return -1
161+
}
162+
}
163+
164+
/**
165+
* One Or More Combinator
166+
*/
167+
168+
class OneOrMore(val combinator : Combinator) : Combinator {
169+
override fun SyntacticMatch(value: String, offset: Int): Int {
170+
var index = offset
171+
var match = combinator.SyntacticMatch(value, index)
172+
173+
if (match == -1) {
174+
return -1
175+
}
176+
177+
while (match != -1) {
178+
index = match
179+
match = combinator.SyntacticMatch(value, index)
180+
}
181+
return index
182+
}
183+
184+
override fun SemanticMatch(value: String, offset: Int): Int {
185+
var index = offset
186+
var match = combinator.SemanticMatch(value, index)
187+
188+
if (match == -1) {
189+
return -1
190+
}
191+
192+
while (match != -1) {
193+
index = match
194+
match = combinator.SemanticMatch(value, index)
195+
}
196+
return index
197+
}
198+
}
199+
200+
/**
201+
* One Or More Combinator
202+
*/
203+
204+
class ZeroOrMore(val combinator : Combinator) : Combinator {
205+
override fun SyntacticMatch(value: String, offset: Int): Int {
206+
var index = offset
207+
var match = combinator.SyntacticMatch(value, index)
208+
209+
if (match == -1) {
210+
return offset
211+
}
212+
213+
while (match != -1) {
214+
index = match
215+
match = combinator.SyntacticMatch(value, index)
216+
}
217+
return index
218+
}
219+
220+
override fun SemanticMatch(value: String, offset: Int): Int {
221+
var index = offset
222+
var match = combinator.SemanticMatch(value, index)
223+
224+
if (match == -1) {
225+
return offset
226+
}
227+
228+
while (match != -1) {
229+
index = match
230+
match = combinator.SemanticMatch(value, index)
231+
}
232+
return index
233+
}
234+
}
235+
236+
237+
class EOF : Combinator {
238+
override fun SyntacticMatch(value: String, offset: Int): Int {
239+
return if (offset == value.length) {
240+
offset
241+
} else {
242+
-1
243+
}
244+
}
245+
246+
override fun SemanticMatch(value: String, offset: Int): Int {
247+
return if (offset == value.length) {
248+
offset
249+
} else {
250+
-1
251+
}
252+
}
253+
}
254+
255+
open class GrammarOptionValue(
256+
override val validatorName: String,
257+
val combinator: Combinator
258+
259+
) : OptionValueInformation {
260+
261+
override fun getAutoCompleteOptions(project: Project): Set<String> {
262+
return emptySet()
263+
}
264+
265+
override fun getErrorMessage(value: String): String? {
266+
val syntaticMatch = combinator.SyntacticMatch(value, 0)
267+
268+
if (syntaticMatch == -1) {
269+
return "Could not parse value"
270+
}
271+
272+
val semanticMatch = combinator.SemanticMatch(value, 0)
273+
274+
if (semanticMatch == -1) {
275+
return "Value did not semantically match"
276+
}
277+
278+
return null
279+
}
280+
}
281+
282+
class IOLimitOptionValue() : GrammarOptionValue("config_parse_io_limit", SequenceCombinator(OneOrMore(SequenceCombinator(BYTES, DEVICE)), EOF())) {
283+
companion object {
284+
val validators = mapOf(
285+
Validator("config_parse_io_limit", "0") to IOLimitOptionValue()
286+
)
287+
}
288+
}
289+
290+
class AllGrammarOptions {
291+
companion object {
292+
val validators = mapOf(
293+
Validator("config_parse_image_policy", "0") to GrammarOptionValue("config_parse_image_policy", IMAGE_POLICY_COMBINATOR),
294+
)
295+
}
296+
}
297+
298+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections
2+
3+
import junit.framework.TestCase
4+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
5+
6+
class InvalidValidInspectionForIOReadBandwidthMax : AbstractUnitFileTest() {
7+
8+
fun testNoWarningWhenNumberSpecifiedWithoutUnit() {
9+
// Fixture Setup
10+
// language="unit file (systemd)"
11+
val file = """
12+
[Service]
13+
IOReadBandwidthMax=2 /home
14+
""".trimIndent()
15+
16+
17+
// Execute SUT
18+
setupFileInEditor("file.service", file)
19+
enableInspection(InvalidValueInspection::class.java)
20+
val highlights = myFixture.doHighlighting()
21+
22+
// Verification
23+
assertSize(0, highlights)
24+
25+
}
26+
27+
fun testWeakWarningWhenNegativeIntegerSpecified() {
28+
// Fixture Setup
29+
// language="unit file (systemd)"
30+
val file = """
31+
[Service]
32+
IOReadBandwidthMax=-5
33+
""".trimIndent()
34+
35+
36+
// Execute SUT
37+
setupFileInEditor("file.service", file)
38+
enableInspection(InvalidValueInspection::class.java)
39+
val highlights = myFixture.doHighlighting()
40+
41+
// Verification
42+
assertSize(1, highlights)
43+
val info = highlights[0]
44+
AbstractUnitFileTest.Companion.assertStringContains("Could not parse value", info!!.description)
45+
TestCase.assertEquals("-5", info.text)
46+
}
47+
48+
}

0 commit comments

Comments
 (0)