mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
840 lines
28 KiB
Python
Executable File
840 lines
28 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate CMake presets documentation (reST) and JSON schema.
|
|
|
|
The documentation and JSON schema for CMake presets are both generated from
|
|
Help/manual/presets/schema.yaml, which describes the schema in a way that
|
|
allows both documentation forms to be combined in a single file, and also makes
|
|
it much easier to manage version revisions (compared to editing the JSON schema
|
|
'by hand').
|
|
|
|
All input and output files are expected to be in known locations relative to
|
|
this script.
|
|
|
|
Usage: python3 regenerate-presets.py
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from copy import deepcopy
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Self
|
|
|
|
import yaml
|
|
|
|
LATEST = 1
|
|
TYPES = {}
|
|
REFS = {}
|
|
|
|
PRESETS_REL = 'Help/manual/presets'
|
|
SCHEMA_YAML_FILENAME = 'schema.yaml'
|
|
SCHEMA_JSON_FILENAME = 'schema.json'
|
|
|
|
WORKSPACE = Path(__file__).parent.parent.parent.absolute()
|
|
SCRIPT_REL = Path(__file__).relative_to(WORKSPACE)
|
|
PRESETS = WORKSPACE / PRESETS_REL
|
|
DIAGNOSTICS = WORKSPACE / 'Source/cmDiagnostics.h'
|
|
|
|
RST_BANNER = f"""\
|
|
.. This file was generated by {SCRIPT_REL.as_posix()}
|
|
from {PRESETS_REL}/{SCHEMA_YAML_FILENAME}. Do not edit.
|
|
"""
|
|
|
|
DIAGNOSTIC_TABLE_MACRO = 'CM_FOR_EACH_DIAGNOSTIC_TABLE'
|
|
CONFIGURE_PRESET_PROPERTIES_PATH = (
|
|
'properties', 'configurePresets', 'items', 'properties'
|
|
)
|
|
WARNING_DESCRIPTION = (
|
|
'An optional boolean. '
|
|
'Equivalent to passing -W{c} or -Wno-{c} on the command line. '
|
|
'This may not be set to false if errors.{p} is set to true.'
|
|
)
|
|
WARNING_SPHINX_DESCRIPTION = """
|
|
An optional boolean. Equivalent to passing :option:`-W{c} <cmake -W>` or
|
|
:option:`-Wno-{c} <cmake -Wno->` on the command line.
|
|
This may not be set to ``false`` if ``errors.{p}`` is set to ``true``.
|
|
""".strip()
|
|
ERROR_DESCRIPTION = (
|
|
'An optional boolean. '
|
|
'Equivalent to passing -Werror={c} or -Wno-error={c} on the command line. '
|
|
'This may not be set to true if warnings.{p} is set to false.'
|
|
)
|
|
ERROR_SPHINX_DESCRIPTION = """
|
|
An optional boolean. Equivalent to passing :cmake-option:`-Werror={c}` or
|
|
:cmake-option:`-Wno-error={c}` on the command line.
|
|
This may not be set to ``true`` if ``warnings.{p}`` is set to ``false``.
|
|
""".strip()
|
|
|
|
|
|
# =============================================================================
|
|
@dataclass
|
|
class Diagnostic:
|
|
presetName: str
|
|
cliName: str
|
|
since: int
|
|
|
|
# -------------------------------------------------------------------------
|
|
def format(self, template: str) -> str:
|
|
return template.format(c=self.cliName, p=self.presetName)
|
|
|
|
|
|
# =============================================================================
|
|
class Value:
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self, data: list[dict[str, Any]]):
|
|
self.data = data
|
|
self.spans = None
|
|
|
|
# -------------------------------------------------------------------------
|
|
@property
|
|
def objects(self) -> dict[str, 'Object']:
|
|
return {}
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateSpans(self, since: int, until: int) -> None:
|
|
self.spans = [(since, until)]
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateRefs(self, objects: dict[str, 'Object']) -> None:
|
|
pass
|
|
|
|
# -------------------------------------------------------------------------
|
|
def defs(self) -> dict[str, Any]:
|
|
return {}
|
|
|
|
# -------------------------------------------------------------------------
|
|
def schema(self, version: int) -> dict[str, Any]:
|
|
return deepcopy(self.data)
|
|
|
|
|
|
# =============================================================================
|
|
class Object(Value):
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self, data: dict[str, Any], name: str,
|
|
label: str | None = None):
|
|
# Prevent data sharing from modifying unrelated parts of the schema as
|
|
# we pick apart our section.
|
|
data = deepcopy(data)
|
|
|
|
self.name = data.pop('id', name)
|
|
self.label = label
|
|
|
|
self.properties = {}
|
|
for n, p in data.pop('properties').items():
|
|
pl = '.'.join([label, n]) if label is not None else n
|
|
self.properties[n] = Property(p, n, pl)
|
|
if len(self.properties):
|
|
self.properties[CommentProperty.name] = CommentProperty()
|
|
|
|
ap = data.pop('additionalProperties', None)
|
|
if ap is not None:
|
|
self.additionalProperties = buildValue(ap, self.name, self.label)
|
|
else:
|
|
self.additionalProperties = None
|
|
|
|
super().__init__(data)
|
|
|
|
# -------------------------------------------------------------------------
|
|
@property
|
|
def objects(self) -> dict[str, Self]:
|
|
out = {self.name: self}
|
|
for p in self.properties.values():
|
|
out.update(p.objects)
|
|
if self.additionalProperties is not None:
|
|
out.update(self.additionalProperties.objects)
|
|
return out
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateSpans(self, since: int, until: int) -> None:
|
|
spans = [[(since, until)]]
|
|
for p in self.properties.values():
|
|
p.updateSpans(since, until)
|
|
spans.append(p.spans)
|
|
if self.additionalProperties is not None:
|
|
self.additionalProperties.updateSpans(since, until)
|
|
spans.append(self.additionalProperties.spans)
|
|
self.spans = combineSpans(*spans)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateRefs(self, objects: dict[str, 'Object']) -> None:
|
|
for p in self.properties.values():
|
|
p.updateRefs(objects)
|
|
if self.additionalProperties is not None:
|
|
self.additionalProperties.updateRefs(objects)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def doc(self, since: int, until: int) -> str | None:
|
|
since = max(since, self.spans[0][0])
|
|
until = min(until, self.spans[-1][1])
|
|
|
|
out = []
|
|
for _, p in self.properties.items():
|
|
content = p.doc(since, until)
|
|
if not content:
|
|
continue
|
|
out.append(content)
|
|
|
|
return '\n\n'.join(out) if len(out) else None
|
|
|
|
# -------------------------------------------------------------------------
|
|
def defs(self, exclude: set[str] = {}) -> dict[str, Any]:
|
|
out = {}
|
|
for n, p in self.properties.items():
|
|
if n in exclude:
|
|
continue
|
|
for s in p.spans:
|
|
r = f'{p.label}@{spanRef(*s)}'
|
|
out[r] = p.schema(s[0])
|
|
out.update(p.defs())
|
|
if self.additionalProperties is not None:
|
|
out.update(self.additionalProperties.defs())
|
|
return out
|
|
|
|
# -------------------------------------------------------------------------
|
|
def schema(self, version: int, inline: set[str] = {}) -> dict[str, Any]:
|
|
out = deepcopy(self.data)
|
|
|
|
props = {}
|
|
required = []
|
|
for n, p in self.properties.items():
|
|
if n in inline:
|
|
props[n] = p.schema(version)
|
|
else:
|
|
r = p.ref(version)
|
|
if r is not None:
|
|
props[n] = r
|
|
if p.required:
|
|
required.append(n)
|
|
|
|
out['properties'] = props
|
|
if len(required):
|
|
out['required'] = required
|
|
|
|
if self.additionalProperties is not None:
|
|
ap = self.additionalProperties.schema(version)
|
|
out['additionalProperties'] = ap
|
|
return out
|
|
|
|
|
|
# =============================================================================
|
|
class Ref(Value):
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self, data: list[dict[str, Any]]):
|
|
self.target = data.pop('target')
|
|
self.targetSpans = None
|
|
super().__init__(data)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateSpans(self, since: int, until: int) -> None:
|
|
if self.spans is None:
|
|
super().updateSpans(since, until)
|
|
self.spans = trimSpans(self.spans, since, until)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateRefs(self, objects: dict[str, 'Object']) -> None:
|
|
global REFS
|
|
|
|
t = objects.get(self.target)
|
|
if t is not None:
|
|
since = self.spans[0][0]
|
|
until = self.spans[-1][1]
|
|
self.spans = trimSpans(t.spans, since, until)
|
|
self.targetSpans = t.spans
|
|
|
|
rs = REFS.get(self.target, (since, until))
|
|
REFS[self.target] = (min(since, rs[0]), max(until, rs[1]))
|
|
|
|
# -------------------------------------------------------------------------
|
|
def schema(self, version: int) -> dict[str, Any]:
|
|
ts = trimSpans(self.targetSpans, *REFS[self.target])
|
|
s = findSpan(ts, version)
|
|
if s is not None:
|
|
return {
|
|
'$ref': f'#/definitions/{self.target}@{spanRef(*s)}',
|
|
}
|
|
return {}
|
|
|
|
|
|
# =============================================================================
|
|
class Array(Value):
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self, data: dict[str, Any], name: str, label: str):
|
|
item = data.pop('items')
|
|
self.value = buildValue(item, name, label)
|
|
|
|
super().__init__(data)
|
|
|
|
# -------------------------------------------------------------------------
|
|
@property
|
|
def objects(self) -> dict[str, Object]:
|
|
return self.value.objects
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateSpans(self, since: int, until: int) -> None:
|
|
self.value.updateSpans(since, until)
|
|
self.spans = self.value.spans
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateRefs(self, objects: dict[str, 'Object']) -> None:
|
|
self.value.updateRefs(objects)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def defs(self) -> dict[str, Any]:
|
|
return self.value.defs()
|
|
|
|
# -------------------------------------------------------------------------
|
|
def schema(self, version: int) -> dict[str, Any]:
|
|
out = deepcopy(self.data)
|
|
out['items'] = self.value.schema(version)
|
|
return out
|
|
|
|
|
|
# =============================================================================
|
|
class Union(Value):
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self, data: list[dict[str, Any]], name: str, label: str):
|
|
self.items = []
|
|
for i, d in enumerate(data):
|
|
self.items.append(buildValue(d, name, self.itemLabel(label, i)))
|
|
|
|
super().__init__({})
|
|
|
|
# -------------------------------------------------------------------------
|
|
def itemLabel(self, label: str, index: int) -> str:
|
|
return label
|
|
|
|
# -------------------------------------------------------------------------
|
|
@property
|
|
def objects(self) -> dict[str, Object]:
|
|
out = {}
|
|
for v in self.items:
|
|
out.update(v.objects)
|
|
return out
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateSpans(self, since: int, until: int) -> None:
|
|
spans = [[(since, until)]]
|
|
for i in self.items:
|
|
i.updateSpans(since, until)
|
|
spans.append(i.spans)
|
|
self.spans = combineSpans(*spans)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateRefs(self, objects: dict[str, 'Object']) -> None:
|
|
for i in self.items:
|
|
i.updateRefs(objects)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def defs(self) -> dict[str, Any]:
|
|
out = {}
|
|
for v in self.items:
|
|
out.update(v.defs())
|
|
return out
|
|
|
|
# -------------------------------------------------------------------------
|
|
def schema(self, version: int) -> dict[str, Any]:
|
|
out = []
|
|
for i in self.items:
|
|
out.append(i.schema(version))
|
|
return {'anyOf': out}
|
|
|
|
|
|
# =============================================================================
|
|
class Variant(Union):
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self, data: list[dict[str, Any]], name: str, label: str):
|
|
self.identifier = data.pop('id', None)
|
|
|
|
if self.identifier is not None:
|
|
name = self.identifier
|
|
label = self.identifier
|
|
|
|
super().__init__(data.pop('items'), name, label)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def itemLabel(self, label: str, index: int) -> str:
|
|
return f'{label}[{index}]'
|
|
|
|
# -------------------------------------------------------------------------
|
|
@property
|
|
def objects(self) -> dict[str, Object]:
|
|
out = super().objects
|
|
if self.identifier is not None:
|
|
out[self.identifier] = self
|
|
return out
|
|
|
|
# -------------------------------------------------------------------------
|
|
def doc(self, since: int, until: int) -> None:
|
|
return None
|
|
|
|
# -------------------------------------------------------------------------
|
|
def defs(self) -> dict[str, Any]:
|
|
out = super().defs()
|
|
|
|
if self.identifier is not None:
|
|
for s in self.spans:
|
|
n = f'{self.identifier}@{spanRef(*s)}'
|
|
out[n] = super().schema(s[0])
|
|
|
|
return out
|
|
|
|
# -------------------------------------------------------------------------
|
|
def schema(self, version: int) -> dict[str, Any]:
|
|
if self.identifier is not None:
|
|
s = findSpan(self.spans, version)
|
|
if s is not None:
|
|
return {
|
|
'$ref': f'#/definitions/{self.identifier}@{spanRef(*s)}',
|
|
}
|
|
return {}
|
|
|
|
return super().schema(version)
|
|
|
|
|
|
# =============================================================================
|
|
class Property:
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self, data: dict[str, Any], name: str, label: str):
|
|
self.name = name
|
|
self.label = label
|
|
self.since = data.pop('since', 1)
|
|
self.until = data.pop('until', LATEST + 1)
|
|
self.required = data.pop('required', False)
|
|
self.content = data.pop('sphinxDescription', data.get('description'))
|
|
|
|
note = data.pop('sphinxNote', None)
|
|
if note is not None:
|
|
self.content += f'\n\n.. note::\n\n{indent(note)}'
|
|
|
|
self._revisions = []
|
|
for rev in data.pop('revisions', []):
|
|
rd = deepcopy(data)
|
|
rd.update(rev)
|
|
self._revisions.append(Property(rd, self.name, self.label))
|
|
|
|
if len(self._revisions):
|
|
self.since = max(self.since,
|
|
min([r.since for r in self._revisions]))
|
|
self.until = min(self.until,
|
|
max([r.until for r in self._revisions]))
|
|
self.value = None
|
|
else:
|
|
self.value = buildValue(data, self.name, self.label)
|
|
|
|
self.spans = None
|
|
|
|
# -------------------------------------------------------------------------
|
|
@property
|
|
def objects(self) -> dict[str, Object]:
|
|
if len(self._revisions):
|
|
out = {}
|
|
for r in self._revisions:
|
|
out.update(r.objects)
|
|
return out
|
|
|
|
return self.value.objects
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateSpans(self, since: int, until: int) -> None:
|
|
since = max(since, self.since)
|
|
until = min(until, self.until)
|
|
|
|
if len(self._revisions):
|
|
spans = [[(since, until)]]
|
|
for r in self._revisions:
|
|
r.updateSpans(since, until)
|
|
spans.append(r.spans)
|
|
self.spans = combineSpans(*spans)
|
|
else:
|
|
self.value.updateSpans(since, until)
|
|
self.spans = self.value.spans
|
|
|
|
# -------------------------------------------------------------------------
|
|
def updateRefs(self, objects: dict[str, 'Object']) -> None:
|
|
if len(self._revisions):
|
|
for r in self._revisions:
|
|
r.updateRefs(objects)
|
|
else:
|
|
self.value.updateRefs(objects)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def ref(self, version: int) -> dict[str, str] | None:
|
|
s = findSpan(self.spans, version)
|
|
if s is not None:
|
|
return {
|
|
'$ref': f'#/definitions/{self.label}@{spanRef(*s)}',
|
|
}
|
|
return None
|
|
|
|
# -------------------------------------------------------------------------
|
|
def doc(self, since: int, until: int) -> str | None:
|
|
if self.content is None:
|
|
return None
|
|
|
|
out = f'.. _`CMakePresets.{self.label}`:\n\n``{self.name}``\n'
|
|
if self.since > since:
|
|
out += f' .. presets-versionadded:: {self.since}\n\n'
|
|
if self.until < until:
|
|
out += f' .. presets-versionremoved:: {self.until}\n\n'
|
|
out += indent(self.content)
|
|
|
|
return out
|
|
|
|
# -------------------------------------------------------------------------
|
|
def defs(self) -> dict[str, Any]:
|
|
if len(self._revisions):
|
|
out = {}
|
|
for r in self._revisions:
|
|
out.update(r.defs())
|
|
return out
|
|
|
|
return self.value.defs()
|
|
|
|
# -------------------------------------------------------------------------
|
|
def schema(self, version: int) -> dict[str, Any]:
|
|
for r in self._revisions:
|
|
if version >= r.since and version < r.until:
|
|
return r.value.schema(version)
|
|
return self.value.schema(version)
|
|
|
|
|
|
# =============================================================================
|
|
class CommentProperty(Property):
|
|
name: str = '$comment'
|
|
since: int = 10
|
|
|
|
# -------------------------------------------------------------------------
|
|
def __init__(self):
|
|
data = {
|
|
'since': type(self).since,
|
|
'id': 'comment',
|
|
'type': 'variant',
|
|
'items': [
|
|
{
|
|
'type': 'string',
|
|
'description': 'A single-line comment.',
|
|
},
|
|
{
|
|
'type': 'array',
|
|
'description': 'A multi-line comment.',
|
|
'minItems': 1,
|
|
'items': {
|
|
'type': 'string',
|
|
'description': 'One line of the multi-line comment.',
|
|
}
|
|
}
|
|
],
|
|
}
|
|
super().__init__(data, type(self).name, type(self).name)
|
|
|
|
# -------------------------------------------------------------------------
|
|
def doc(self, since: int, until: int) -> None:
|
|
return None
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def buildValue(
|
|
data: Any, name: str, label: str | None = None
|
|
) -> Variant | Object | Array | Ref | Value:
|
|
if type(data) is dict:
|
|
if 'anyOf' in data:
|
|
return Union(data.pop('anyOf'), name, label)
|
|
elif data['type'] == 'variant':
|
|
return Variant(data, name, label)
|
|
elif data['type'] == 'array':
|
|
return Array(data, name, label)
|
|
elif data['type'] == 'object':
|
|
return Object(data, name, label)
|
|
elif data['type'] == 'ref':
|
|
return Ref(data)
|
|
|
|
return Value(data)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def extractDivisions(spans: list[tuple[int, int]]) -> set[int]:
|
|
return {n for s in spans for n in s}
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def trimSpans(spans: list[tuple[int, int]],
|
|
since: int, until: int) -> list[tuple[int, int]]:
|
|
divisions = extractDivisions(spans)
|
|
|
|
last = since
|
|
out = []
|
|
|
|
for d in sorted(divisions):
|
|
if d > since:
|
|
out.append((last, min(d, until)))
|
|
last = min(d, until)
|
|
if d >= until:
|
|
break
|
|
|
|
return out
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def combineSpans(*spans: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
divisions = set()
|
|
for s in spans:
|
|
divisions.update(extractDivisions(s))
|
|
|
|
divisions = sorted(divisions)
|
|
last = divisions.pop(0)
|
|
out = []
|
|
|
|
for d in divisions:
|
|
out.append((last, d))
|
|
last = d
|
|
|
|
return out
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def findSpan(spans: list[tuple[int, int]],
|
|
version: int) -> tuple[int, int] | None:
|
|
for s in spans:
|
|
if version >= s[0] and version < s[1]:
|
|
return s
|
|
|
|
return None
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def spanRef(since: int, until: int) -> str:
|
|
if since + 1 < until:
|
|
if until > LATEST:
|
|
return f'v{since}..'
|
|
else:
|
|
return f'v{since}..v{until - 1}'
|
|
return f'v{since}'
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def indent(text: str):
|
|
return '\n'.join([(' ' + line).rstrip() for line in text.split('\n')])
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def trimDescriptions(data: Any):
|
|
if type(data) is dict:
|
|
for k, v in data.items():
|
|
if type(v) is str and k in {'description', 'sphinxDescription'}:
|
|
data[k] = v.strip()
|
|
elif type(v) is dict:
|
|
data[k] = trimDescriptions(v)
|
|
elif type(v) is list:
|
|
data[k] = [trimDescriptions(item) for item in v]
|
|
|
|
return data
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def getPath(data: Any, path: tuple[str]) -> Any:
|
|
if len(path) == 0:
|
|
return data
|
|
return getPath(data[path[0]], path[1:])
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def diagnosticPresetName(symbol: str) -> str:
|
|
sep = False
|
|
out = ''
|
|
for c in symbol[4:]:
|
|
if c == '_':
|
|
sep = True
|
|
elif sep:
|
|
out += c
|
|
sep = False
|
|
else:
|
|
out += c.lower()
|
|
return out
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def buildDiagnosticsSchema(
|
|
diagnostics: list[Diagnostic],
|
|
descriptionTemplate: str,
|
|
sphinxDescriptionTemplate: str,
|
|
) -> dict[str, dict[str, Any]]:
|
|
out = {}
|
|
|
|
for d in diagnostics:
|
|
out[d.presetName] = {
|
|
'since': d.since,
|
|
'type': 'boolean',
|
|
'description': d.format(descriptionTemplate),
|
|
'sphinxDescription': d.format(sphinxDescriptionTemplate),
|
|
}
|
|
|
|
return out
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def mergeDiagnostics(
|
|
generated: dict[str, dict[str, Any]],
|
|
extra: dict[str, dict[str, Any]],
|
|
) -> dict[str, dict[str, Any]]:
|
|
# Get unsorted names.
|
|
tail = {}
|
|
for k in list(extra.keys()):
|
|
s = extra[k].pop('sort', True)
|
|
if not s:
|
|
tail[k] = extra.pop(k)
|
|
|
|
# Combine inputs and sort.
|
|
out = {}
|
|
combined = deepcopy(generated)
|
|
combined.update(extra)
|
|
for k in sorted(combined.keys()):
|
|
out[k] = combined[k]
|
|
|
|
# Add unsorted items and return result.
|
|
out.update(tail)
|
|
return out
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def readDiagnostics(path: Path | str) -> list[Diagnostic]:
|
|
content = ''
|
|
|
|
# Extract diagnostics table from header.
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
extracting = False
|
|
for line in f:
|
|
if DIAGNOSTIC_TABLE_MACRO in line:
|
|
extracting = True
|
|
continue
|
|
|
|
if extracting:
|
|
content += line.replace('\\\n', ' ')
|
|
if not line.strip().endswith('\\'):
|
|
break
|
|
|
|
out = []
|
|
while True:
|
|
m = re.match(r'\s*SELECT[(]([^)]+)[)]', content)
|
|
if m is None:
|
|
break
|
|
|
|
args = [a.strip() for a in m.group(1).split(',')]
|
|
p = diagnosticPresetName(args[3])
|
|
c = args[3][4:].lower().replace('_', '-')
|
|
v = int(args[4])
|
|
if v > 1:
|
|
out.append(Diagnostic(p, c, v))
|
|
|
|
content = content[m.span()[1]:]
|
|
|
|
return out
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
def main():
|
|
# Read the schema definition.
|
|
with open(PRESETS / SCHEMA_YAML_FILENAME, 'r', encoding='utf-8') as f:
|
|
schema = yaml.safe_load(f)
|
|
|
|
# Extract the current (latest) version.
|
|
global LATEST
|
|
|
|
del schema['definitions']
|
|
LATEST = schema.pop('version')
|
|
|
|
# Remove whitespace around descriptions.
|
|
schema = trimDescriptions(schema)
|
|
|
|
# Read diagnostics and update schema.
|
|
diagnostics = readDiagnostics(DIAGNOSTICS)
|
|
configureSchema = getPath(schema, CONFIGURE_PRESET_PROPERTIES_PATH)
|
|
configureSchema['warnings']['properties'] = mergeDiagnostics(
|
|
buildDiagnosticsSchema(diagnostics, WARNING_DESCRIPTION,
|
|
WARNING_SPHINX_DESCRIPTION),
|
|
configureSchema['warnings']['properties'])
|
|
configureSchema['errors']['properties'] = mergeDiagnostics(
|
|
buildDiagnosticsSchema(diagnostics, ERROR_DESCRIPTION,
|
|
ERROR_SPHINX_DESCRIPTION),
|
|
configureSchema['errors']['properties'])
|
|
|
|
# Extract global type definitions.
|
|
global TYPES, REFS
|
|
|
|
for t in schema.pop('types', []):
|
|
name = t['id']
|
|
value = buildValue(t, name)
|
|
value.updateSpans(1, LATEST + 1)
|
|
TYPES[name] = value
|
|
|
|
for t in TYPES.values():
|
|
t.updateRefs(TYPES)
|
|
|
|
# Reset the spans for references, as these currently contain only spans for
|
|
# circular references of global types, which are likely broader than the
|
|
# actual usage spans of those types. We'll recalculate the correct spans
|
|
# when we update refs from the root object.
|
|
REFS = {}
|
|
|
|
# Parse the root object. This will also recursively parse other objects.
|
|
root = Object(schema, 'root')
|
|
root.updateSpans(1, LATEST + 1)
|
|
|
|
# Resolve references.
|
|
objects = root.objects
|
|
|
|
types = objects
|
|
types.update(TYPES)
|
|
|
|
root.updateRefs(types)
|
|
|
|
for name, value in TYPES.items():
|
|
span = REFS.get(name)
|
|
if span is not None:
|
|
value.updateSpans(*span)
|
|
|
|
root.updateSpans(1, LATEST + 1)
|
|
|
|
# Extract Sphinx documentation for each object's properties.
|
|
for n, o in objects.items():
|
|
doc = o.doc(1, LATEST + 1)
|
|
if doc is not None:
|
|
print(f'- Generating reST documentation for {n} properties')
|
|
with open(PRESETS / f'{o.name}-properties.rst', 'w', encoding='utf-8') as f:
|
|
print(RST_BANNER, file=f)
|
|
print(doc, file=f)
|
|
|
|
# Generate schema for each version.
|
|
versions = []
|
|
for v in range(1, LATEST + 1):
|
|
vs = root.schema(v, inline={'version'})
|
|
vs['properties']['version']['const'] = v
|
|
versions.append(vs)
|
|
|
|
# Add definitions and write the schema.
|
|
types = root.defs(exclude={'version'})
|
|
for t in TYPES.values():
|
|
types.update(t.defs())
|
|
|
|
schema = {
|
|
'$schema': 'http://json-schema.org/draft/2020-12/schema#',
|
|
'type': 'object',
|
|
'oneOf': versions,
|
|
'required': ['version'],
|
|
'definitions': types
|
|
}
|
|
|
|
with open(PRESETS / SCHEMA_JSON_FILENAME, 'w', encoding='utf-8') as f:
|
|
print(json.dumps(schema, indent=2, ensure_ascii=False), file=f)
|
|
|
|
|
|
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
main()
|