[PATCH v1 06/10] kconfig-checker: Split schema to support component reuse

From: Ahmed S. Darwish

Date: Fri Nov 06 2026 - 04:21:27 EST


Normal rule files require every rule to define both 'expected' and
'severity'. Overlay files are different: an overlay may update only
selected fields of an existing rule, so a future overlay schema will permit
these fields to be omitted.

Move the common rule definitions into schemas/base.yml and add a strict
schemas/rules.yml wrapper for complete rule files.

Teach the checker to build a Draft7Validator with local file:// URIs so
that rules.yml can resolve base.yml.

Signed-off-by: Ahmed S. Darwish <darwi@xxxxxxxxxxxxx>
---
tools/kconfig-checker/check.py | 52 ++++++++++++++++---
.../{schema.yml => schemas/base.yml} | 3 +-
tools/kconfig-checker/schemas/rules.yml | 20 +++++++
3 files changed, 65 insertions(+), 10 deletions(-)
rename tools/kconfig-checker/{schema.yml => schemas/base.yml} (97%)
create mode 100644 tools/kconfig-checker/schemas/rules.yml

diff --git a/tools/kconfig-checker/check.py b/tools/kconfig-checker/check.py
index ef49c28b9dab..4f79ec487da7 100755
--- a/tools/kconfig-checker/check.py
+++ b/tools/kconfig-checker/check.py
@@ -80,6 +80,8 @@ except ModuleNotFoundError as e:
# Actual script:
#

+from jsonschema import Draft7Validator
+
class KernelConfig:
""".config file parser"""

@@ -133,9 +135,9 @@ class Helpers:
error(f'Invalid YAML syntax at {yaml_f}:\n {e}', os.EX_DATAERR)

@staticmethod
- def validate_yaml(yaml: YamlDict, schema: YamlDict, f_name: str) -> None:
+ def validate_yaml(yaml: YamlDict, validator: Draft7Validator, f_name: str) -> None:
try:
- jsonschema.validate(yaml, schema)
+ validator.validate(yaml)
except jsonschema.exceptions.ValidationError as e:
error(
f'{f_name} does not match its schema\n'
@@ -144,12 +146,45 @@ class Helpers:
)

class Schema:
- """Rules file schema, in json-schema.org format"""
+ """Rules file schema, in json-schema.org draft-07 format"""
+
+ # Rules schema refer to definitions from this base schema
+ BASE_SCHEMA = 'base.yml'

def __init__(self, schema_f: Path):
- self.schema = Helpers.load_yaml(schema_f)
- # Validate the schema itself against draft-07 metaschema
- jsonschema.Draft7Validator.check_schema(self.schema)
+ base_schema_f = schema_f.parent / self.BASE_SCHEMA
+
+ # Use file:// URIs so jsonschema can resolve local '$ref' entries
+ base_schema_uri = base_schema_f.resolve().as_uri()
+ schema_uri = schema_f.resolve().as_uri()
+
+ base_schema = self._load(base_schema_f)
+ self.schema = self._load(schema_f)
+
+ self.validator = self._create_validator(
+ base_schema_uri, base_schema,
+ schema_uri, self.schema,
+ )
+
+ @staticmethod
+ def _create_validator(base_schema_uri: str, base_schema: YamlDict,
+ schema_uri: str, schema: YamlDict) -> Draft7Validator:
+ """Create a schema validator which can resolve local '$ref' URIs"""
+ resolver = jsonschema.RefResolver(
+ base_uri=schema_uri,
+ referrer=schema,
+ store={ base_schema_uri: base_schema },
+ )
+
+ return Draft7Validator(schema, resolver=resolver)
+
+ @staticmethod
+ def _load(path: Path) -> YamlDict:
+ """Load and validate the schema itself against its metaschema"""
+ schema = Helpers.load_yaml(path)
+
+ Draft7Validator.check_schema(schema)
+ return schema

def as_dict(self) -> YamlDict:
return self.schema
@@ -273,7 +308,7 @@ class RulesFile:

def __init__(self, yaml_f: Path, schema: Schema):
self.yaml = Helpers.load_yaml(yaml_f)
- Helpers.validate_yaml(self.yaml, schema.as_dict(), yaml_f.name)
+ Helpers.validate_yaml(self.yaml, schema.validator, yaml_f.name)

def parse(self) -> Rules:
"""Parse the YAML rules into a python Rules object"""
@@ -533,6 +568,7 @@ def parse_args(rules: list[str]) -> argparse.Namespace:
def main() -> None:
base_dir = Path(__file__).resolve().parent
rules_dir = base_dir / 'rules'
+ schema_dir = base_dir / 'schemas'

rules_l = sorted(path.stem for path in rules_dir.glob('*.yml'))
args = parse_args(rules_l)
@@ -544,7 +580,7 @@ def main() -> None:
kconfig_f = args.config.resolve()
kconfig = KernelConfig(kconfig_f)

- schema_f = base_dir / 'schema.yml'
+ schema_f = schema_dir / 'rules.yml'
schema = Schema(schema_f)

rules_f = rules_dir / f'{args.rules}.yml'
diff --git a/tools/kconfig-checker/schema.yml b/tools/kconfig-checker/schemas/base.yml
similarity index 97%
rename from tools/kconfig-checker/schema.yml
rename to tools/kconfig-checker/schemas/base.yml
index a7f1b0a78cc7..1cc19752ebd6 100644
--- a/tools/kconfig-checker/schema.yml
+++ b/tools/kconfig-checker/schemas/base.yml
@@ -2,7 +2,7 @@

"$schema": "https://json-schema.org/draft-07/schema";

-title: Schema for kconfig-checker rule files
+title: Base schema for kconfig-checker rule files
type: object
required: [rules]
additionalProperties: false
@@ -24,7 +24,6 @@ properties:
oneOf:
- required: [name]
- required: [name_pattern]
- required: [expected, severity]
additionalProperties: false

properties:
diff --git a/tools/kconfig-checker/schemas/rules.yml b/tools/kconfig-checker/schemas/rules.yml
new file mode 100644
index 000000000000..90f0eeef497f
--- /dev/null
+++ b/tools/kconfig-checker/schemas/rules.yml
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+"$schema": "https://json-schema.org/draft-07/schema#";
+
+title: Schema for kconfig-checker rule files
+
+allOf:
+ - $ref: "base.yml"
+
+ #
+ # The base schema leaves 'expected' and 'severity' optional so that
+ # overlay files can override only selected fields. In a full rules
+ # file though, these fields are mandatory for each rule.
+ #
+ - type: object
+ properties:
+ rules:
+ type: array
+ items:
+ required: [expected, severity]
--
2.56.0