Skip to content

Commit b93d5ea

Browse files
Steve Ramageclaude
andcommitted
feat: add validators for tasks_max, unit_slice, tbf_size, nsec, permille (Resolves #442)
Each grammar mirrors the systemd C parser semantics rather than guessing from man-page prose: - config_parse_tasks_max: "infinity" | positive uint64 | 0..100(.NN)?% (parse_permyriad) - config_parse_unit_slice: single token, ".slice" suffix unless containing a specifier - config_parse_tbf_size (QDISC_KIND_TBF): IEC byte-size grammar, same shape as config_parse_fq_size - config_parse_nsec: existing TIME_VALUE grammar (parse_nsec accepts the same syntax as parse_sec) - config_parse_permille: 0..100(.x)?% (ASCII percent form of parse_permille) Reduces OptionValueTest missing-function count from 405 to 400 and adds 25 newly-validated key occurrences across [Service], [Mount], [Socket], [Swap], [TokenBucketFilter], and [CAN] sections. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6241d15 commit b93d5ea

11 files changed

Lines changed: 385 additions & 0 deletions

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
133133
Validator("config_parse_nexthop_section", "NEXTHOP_GROUP") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation,
134134
Validator("config_parse_nexthop_section", "NEXTHOP_ID") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation,
135135
Validator("config_parse_nexthop_section", "NEXTHOP_ONLINK") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation,
136+
Validator("config_parse_nsec", "0") to ConfigParseNsecOptionValue() as OptionValueInformation,
137+
Validator("config_parse_permille", "0") to ConfigParsePermilleOptionValue() as OptionValueInformation,
136138
Validator("config_parse_pfifo_size", "QDISC_KIND_PFIFO") to ConfigParsePfifoSizeOptionValue() as OptionValueInformation,
137139
Validator("config_parse_pfifo_size", "QDISC_KIND_PFIFO_HEAD_DROP") to ConfigParsePfifoSizeOptionValue() as OptionValueInformation,
138140
Validator("config_parse_pid2", "0") to ConfigParsePid2OptionValue() as OptionValueInformation,
@@ -167,6 +169,8 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
167169
Validator("config_parse_sr_iov_num_vfs", "0") to ConfigParseSrIovNumVfsOptionValue() as OptionValueInformation,
168170
Validator("config_parse_sr_iov_vlan_proto", "0") to ConfigParseSrIovVlanProtoOptionValue() as OptionValueInformation,
169171
Validator("config_parse_swap_priority", "0") to ConfigParseSwapPriorityOptionValue() as OptionValueInformation,
172+
Validator("config_parse_tasks_max", "0") to ConfigParseTasksMaxOptionValue() as OptionValueInformation,
173+
Validator("config_parse_tbf_size", "QDISC_KIND_TBF") to ConfigParseTbfSizeOptionValue() as OptionValueInformation,
170174
Validator("config_parse_tcp_window", "0") to ConfigParseTcpWindowOptionValue() as OptionValueInformation,
171175
Validator("config_parse_timezone_mode", "0") to ConfigParseTimezoneModeOptionValue() as OptionValueInformation,
172176
Validator("config_parse_trigger_unit", "0") to ConfigParseTriggerUnitOptionValue() as OptionValueInformation,
@@ -176,6 +180,7 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
176180
Validator("config_parse_unit_condition_string", "CONDITION_CONTROL_GROUP_CONTROLLER") to ConfigParseUnitConditionStringOptionValue() as OptionValueInformation,
177181
Validator("config_parse_unit_condition_string", "CONDITION_CPU_FEATURE") to ConfigParseUnitConditionStringOptionValue() as OptionValueInformation,
178182
Validator("config_parse_unit_condition_string", "CONDITION_FIRST_BOOT") to ConfigParseUnitConditionStringOptionValue() as OptionValueInformation,
183+
Validator("config_parse_unit_slice", "0") to ConfigParseUnitSliceOptionValue() as OptionValueInformation,
179184
Validator("config_parse_use_domains", "0") to ConfigParseUseDomainsOptionValue() as OptionValueInformation,
180185
Validator("config_parse_userns_chown", "0") to ConfigParseUsernsChownOptionValue() as OptionValueInformation,
181186
Validator("config_parse_userns_ownership", "0") to ConfigParseUsernsOwnershipOptionValue() as OptionValueInformation,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for nanosecond-time options such as TimerSlackNSec=.
8+
*
9+
* C function: defined via DEFINE_PARSER(nsec, ..., parse_nsec) in src/shared/conf-parser.c,
10+
* which delegates to parse_nsec in src/basic/time-util.c. parse_nsec accepts "infinity" or
11+
* one or more numeric terms with optional unit suffix (s/ms/us/ns/min/h/d/w/M/y...). The same
12+
* syntax that TIME_VALUE encodes for the parse_sec family applies here; only the default
13+
* (suffix-less) multiplier differs.
14+
*/
15+
class ConfigParseNsecOptionValue : SimpleGrammarOptionValues(
16+
"config_parse_nsec",
17+
SequenceCombinator(
18+
OptionalWhitespacePrefix(TIME_VALUE),
19+
EOF()
20+
)
21+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for permille (parts-per-thousand) options such as CAN.SamplePoint=.
8+
*
9+
* C function: config_parse_permille in src/shared/conf-parser.c → parse_permille in
10+
* src/basic/percent-util.c. Accepts either:
11+
* - "N‰" (integer 0..1000)
12+
* - "N%" or "N.x%" (0..100% via tenths place, internally translated to 0..1000)
13+
*
14+
* Only the ASCII percent form is matched here; the ‰ Unicode suffix is syntactically rare.
15+
*/
16+
class ConfigParsePermilleOptionValue : SimpleGrammarOptionValues(
17+
"config_parse_permille",
18+
SequenceCombinator(
19+
AlternativeCombinator(
20+
// 0..99(.x)?%
21+
SequenceCombinator(IntegerTerminal(0, 100), ZeroOrOne(RegexTerminal("\\.[0-9]", "\\.[0-9]")), LiteralChoiceTerminal("%")),
22+
// 100% or 100.0%
23+
SequenceCombinator(LiteralChoiceTerminal("100"), ZeroOrOne(LiteralChoiceTerminal(".0")), LiteralChoiceTerminal("%"))
24+
),
25+
EOF()
26+
)
27+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for TasksMax=.
8+
*
9+
* C function: config_parse_tasks_max in src/core/load-fragment.c. The accepted values are:
10+
* - "infinity"
11+
* - a permyriad form: parse_permyriad accepts N%, N.N%, N.NN%, N‰, N.N‰, or N‱
12+
* (bounded 0..10000). Only the ASCII percent form is matched here; the ‰/‱ Unicode
13+
* suffixes are syntactically rare in unit files.
14+
* - a strictly positive uint64 via safe_atou64 (>0 and <UINT64_MAX).
15+
*
16+
* Suffix-less "0" is rejected by the C parser (safe_atou64 returns 0 which fails the v<=0
17+
* guard); "0%" is accepted (permyriad 0). The grammar below reflects that asymmetry.
18+
*/
19+
class ConfigParseTasksMaxOptionValue : SimpleGrammarOptionValues(
20+
"config_parse_tasks_max",
21+
SequenceCombinator(
22+
AlternativeCombinator(
23+
FlexibleLiteralChoiceTerminal("infinity"),
24+
// Permyriad via %: 0..100 with up to two decimal places.
25+
SequenceCombinator(
26+
AlternativeCombinator(
27+
SequenceCombinator(IntegerTerminal(0, 100), ZeroOrOne(RegexTerminal("\\.[0-9]{1,2}", "\\.[0-9]{1,2}"))),
28+
SequenceCombinator(LiteralChoiceTerminal("100"), ZeroOrOne(RegexTerminal("\\.0{1,2}", "\\.0{1,2}")))
29+
),
30+
LiteralChoiceTerminal("%")
31+
),
32+
// Strictly positive integer.
33+
RegexTerminal("[0-9]+", "[1-9][0-9]*")
34+
),
35+
EOF()
36+
)
37+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for the TokenBucketFilter size-style options: BurstBytes, LimitBytes, MTUBytes,
8+
* MPUBytes (and the deprecated Burst / LimitSize aliases).
9+
*
10+
* C function: config_parse_tbf_size(QDISC_KIND_TBF) in src/network/tc/tbf.c. After branching
11+
* on the lvalue, it calls parse_size(rvalue, 1024, &k), so the value is a decimal byte count
12+
* optionally suffixed with an IEC unit. Mirrors the existing config_parse_fq_size validator.
13+
*/
14+
class ConfigParseTbfSizeOptionValue : SimpleGrammarOptionValues(
15+
"config_parse_tbf_size",
16+
SequenceCombinator(
17+
OptionalWhitespacePrefix(BYTES),
18+
EOF()
19+
)
20+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for Slice= (the unit's enclosing slice).
8+
*
9+
* C function: config_parse_unit_slice in src/core/load-fragment.c. It calls unit_name_printf
10+
* (which expands specifiers like %p/%n) and then manager_load_unit, which validates that the
11+
* resolved name is a real slice unit. Pre-expansion, the value must be a single token; if it
12+
* contains specifiers it can produce anything, otherwise it must already look like a slice
13+
* (ends in ".slice"). Whitespace-separated lists are not accepted.
14+
*/
15+
class ConfigParseUnitSliceOptionValue : SimpleGrammarOptionValues(
16+
"config_parse_unit_slice",
17+
SequenceCombinator(
18+
AlternativeCombinator(
19+
// Literal slice unit name: no whitespace, must end in .slice
20+
RegexTerminal("\\S+", "[^\\s%]+\\.slice"),
21+
// Contains a specifier — expansion can produce any unit name
22+
RegexTerminal("\\S+", "\\S*%\\S+|\\S+%\\S*")
23+
),
24+
EOF()
25+
)
26+
)
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.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
4+
import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection
5+
import org.junit.Test
6+
7+
class ConfigParseNsecOptionValueTest : AbstractUnitFileTest() {
8+
9+
@Test
10+
fun testValidValues() {
11+
// language="unit file (systemd)"
12+
val file = """
13+
[Service]
14+
TimerSlackNSec=infinity
15+
TimerSlackNSec=50
16+
TimerSlackNSec=100us
17+
TimerSlackNSec=1ms
18+
TimerSlackNSec=10s
19+
TimerSlackNSec=1min
20+
TimerSlackNSec=2h
21+
TimerSlackNSec=1d
22+
TimerSlackNSec=1s 500ms
23+
""".trimIndent()
24+
25+
setupFileInEditor("file.service", file)
26+
enableInspection(InvalidValueInspection::class.java)
27+
val highlights = myFixture.doHighlighting()
28+
29+
assertSize(0, highlights)
30+
}
31+
32+
@Test
33+
fun testInvalidValues() {
34+
// language="unit file (systemd)"
35+
val file = """
36+
[Service]
37+
TimerSlackNSec=<error descr="Invalid value">abc</error>
38+
TimerSlackNSec=<error descr="Invalid value">-1</error>
39+
TimerSlackNSec=<error descr="Invalid value">10zz</error>
40+
""".trimIndent()
41+
42+
setupFileInEditor("file.service", file)
43+
enableInspection(InvalidValueInspection::class.java)
44+
val highlights = myFixture.doHighlighting()
45+
46+
assertSize(3, highlights)
47+
}
48+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
4+
import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection
5+
import org.junit.Test
6+
7+
class ConfigParsePermilleOptionValueTest : AbstractUnitFileTest() {
8+
9+
@Test
10+
fun testValidValues() {
11+
// language="unit file (systemd)"
12+
val file = """
13+
[CAN]
14+
SamplePoint=0%
15+
SamplePoint=50%
16+
SamplePoint=87.5%
17+
SamplePoint=99%
18+
SamplePoint=99.9%
19+
SamplePoint=100%
20+
SamplePoint=100.0%
21+
DataSamplePoint=75%
22+
DataSamplePoint=12.3%
23+
""".trimIndent()
24+
25+
setupFileInEditor("file.network", file)
26+
enableInspection(InvalidValueInspection::class.java)
27+
val highlights = myFixture.doHighlighting()
28+
29+
assertSize(0, highlights)
30+
}
31+
32+
@Test
33+
fun testInvalidValues() {
34+
// language="unit file (systemd)"
35+
val file = """
36+
[CAN]
37+
SamplePoint=<error descr="Invalid value">50</error>
38+
SamplePoint=<error descr="Invalid value">100.5%</error>
39+
SamplePoint=<error descr="Invalid value">101%</error>
40+
SamplePoint=<error descr="Invalid value">200%</error>
41+
SamplePoint=<error descr="Invalid value">abc</error>
42+
SamplePoint=<error descr="Invalid value">50.12%</error>
43+
SamplePoint=<error descr="Invalid value">-5%</error>
44+
""".trimIndent()
45+
46+
setupFileInEditor("file.network", file)
47+
enableInspection(InvalidValueInspection::class.java)
48+
val highlights = myFixture.doHighlighting()
49+
50+
assertSize(7, highlights)
51+
}
52+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
4+
import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection
5+
import org.junit.Test
6+
7+
class ConfigParseTasksMaxOptionValueTest : AbstractUnitFileTest() {
8+
9+
@Test
10+
fun testValidValues() {
11+
// language="unit file (systemd)"
12+
val file = """
13+
[Service]
14+
TasksMax=infinity
15+
TasksMax=1
16+
TasksMax=512
17+
TasksMax=999999999
18+
TasksMax=0%
19+
TasksMax=50%
20+
TasksMax=99.9%
21+
TasksMax=12.34%
22+
TasksMax=100%
23+
TasksMax=100.0%
24+
TasksMax=100.00%
25+
""".trimIndent()
26+
27+
setupFileInEditor("file.service", file)
28+
enableInspection(InvalidValueInspection::class.java)
29+
val highlights = myFixture.doHighlighting()
30+
31+
assertSize(0, highlights)
32+
}
33+
34+
@Test
35+
fun testInvalidValues() {
36+
// language="unit file (systemd)"
37+
val file = """
38+
[Service]
39+
TasksMax=<error descr="Invalid value">0</error>
40+
TasksMax=<error descr="Invalid value">-1</error>
41+
TasksMax=<error descr="Invalid value">abc</error>
42+
TasksMax=<error descr="Invalid value">100.5%</error>
43+
TasksMax=<error descr="Invalid value">250%</error>
44+
TasksMax=<error descr="Invalid value">50.123%</error>
45+
TasksMax=<error descr="Invalid value">infinity 1</error>
46+
""".trimIndent()
47+
48+
setupFileInEditor("file.service", file)
49+
enableInspection(InvalidValueInspection::class.java)
50+
val highlights = myFixture.doHighlighting()
51+
52+
assertSize(7, highlights)
53+
}
54+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
4+
import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection
5+
import org.junit.Test
6+
7+
class ConfigParseTbfSizeOptionValueTest : AbstractUnitFileTest() {
8+
9+
@Test
10+
fun testValidValues() {
11+
// language="unit file (systemd)"
12+
val file = """
13+
[TokenBucketFilter]
14+
BurstBytes=1024
15+
LimitBytes=64K
16+
MTUBytes=1500
17+
MPUBytes=64
18+
BurstBytes=1M
19+
LimitBytes=1G
20+
MTUBytes=2T
21+
""".trimIndent()
22+
23+
setupFileInEditor("file.network", file)
24+
enableInspection(InvalidValueInspection::class.java)
25+
val highlights = myFixture.doHighlighting()
26+
27+
assertSize(0, highlights)
28+
}
29+
30+
@Test
31+
fun testInvalidValues() {
32+
// language="unit file (systemd)"
33+
val file = """
34+
[TokenBucketFilter]
35+
BurstBytes=<error descr="Invalid value">abc</error>
36+
LimitBytes=<error descr="Invalid value">-1</error>
37+
MTUBytes=<error descr="Invalid value">10X</error>
38+
""".trimIndent()
39+
40+
setupFileInEditor("file.network", file)
41+
enableInspection(InvalidValueInspection::class.java)
42+
val highlights = myFixture.doHighlighting()
43+
44+
assertSize(3, highlights)
45+
}
46+
}

0 commit comments

Comments
 (0)