Skip to content

Commit 90d4861

Browse files
authored
Merge pull request github#10875 from github/redsun82/swift-codegen-doc
Swift: add infrastructure for documenting generated code
2 parents fd226c5 + 6bd09b1 commit 90d4861

File tree

143 files changed

+2628
-57
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+2628
-57
lines changed

swift/codegen/generators/qlgen.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,38 +55,92 @@ class NoClasses(Error):
5555
pass
5656

5757

58+
abbreviations = {
59+
"expr": "expression",
60+
"arg": "argument",
61+
"stmt": "statement",
62+
"decl": "declaration",
63+
"repr": "representation",
64+
"param": "parameter",
65+
"int": "integer",
66+
}
67+
68+
abbreviations.update({f"{k}s": f"{v}s" for k, v in abbreviations.items()})
69+
70+
_abbreviations_re = re.compile("|".join(fr"\b{abbr}\b" for abbr in abbreviations))
71+
72+
73+
def _humanize(s: str) -> str:
74+
ret = inflection.humanize(s)
75+
ret = ret[0].lower() + ret[1:]
76+
ret = _abbreviations_re.sub(lambda m: abbreviations[m[0]], ret)
77+
return ret
78+
79+
80+
_format_re = re.compile(r"\{(\w+)\}")
81+
82+
83+
def _get_doc(cls: schema.Class, prop: schema.Property, plural=None):
84+
if prop.doc:
85+
if plural is None:
86+
# for consistency, ignore format in non repeated properties
87+
return _format_re.sub(lambda m: m[1], prop.doc)
88+
format = prop.doc
89+
nouns = [m[1] for m in _format_re.finditer(prop.doc)]
90+
if not nouns:
91+
noun, _, rest = prop.doc.partition(" ")
92+
format = f"{{{noun}}} {rest}"
93+
nouns = [noun]
94+
transform = inflection.pluralize if plural else inflection.singularize
95+
return format.format(**{noun: transform(noun) for noun in nouns})
96+
97+
prop_name = _humanize(prop.name)
98+
class_name = cls.default_doc_name or _humanize(inflection.underscore(cls.name))
99+
if prop.is_predicate:
100+
return f"this {class_name} {prop_name}"
101+
if plural is not None:
102+
prop_name = inflection.pluralize(prop_name) if plural else inflection.singularize(prop_name)
103+
return f"{prop_name} of this {class_name}"
104+
105+
58106
def get_ql_property(cls: schema.Class, prop: schema.Property, prev_child: str = "") -> ql.Property:
59107
args = dict(
60108
type=prop.type if not prop.is_predicate else "predicate",
61109
qltest_skip="qltest_skip" in prop.pragmas,
62110
prev_child=prev_child if prop.is_child else None,
63111
is_optional=prop.is_optional,
64112
is_predicate=prop.is_predicate,
113+
description=prop.description
65114
)
66115
if prop.is_single:
67116
args.update(
68117
singular=inflection.camelize(prop.name),
69118
tablename=inflection.tableize(cls.name),
70119
tableparams=["this"] + ["result" if p is prop else "_" for p in cls.properties if p.is_single],
120+
doc=_get_doc(cls, prop),
71121
)
72122
elif prop.is_repeated:
73123
args.update(
74124
singular=inflection.singularize(inflection.camelize(prop.name)),
75125
plural=inflection.pluralize(inflection.camelize(prop.name)),
76126
tablename=inflection.tableize(f"{cls.name}_{prop.name}"),
77127
tableparams=["this", "index", "result"],
128+
doc=_get_doc(cls, prop, plural=False),
129+
doc_plural=_get_doc(cls, prop, plural=True),
78130
)
79131
elif prop.is_optional:
80132
args.update(
81133
singular=inflection.camelize(prop.name),
82134
tablename=inflection.tableize(f"{cls.name}_{prop.name}"),
83135
tableparams=["this", "result"],
136+
doc=_get_doc(cls, prop),
84137
)
85138
elif prop.is_predicate:
86139
args.update(
87140
singular=inflection.camelize(prop.name, uppercase_first_letter=False),
88141
tablename=inflection.underscore(f"{cls.name}_{prop.name}"),
89142
tableparams=["this"],
143+
doc=_get_doc(cls, prop),
90144
)
91145
else:
92146
raise ValueError(f"unknown property kind for {prop.name} from {cls.name}")
@@ -109,6 +163,7 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
109163
properties=properties,
110164
dir=pathlib.Path(cls.group or ""),
111165
ipa=bool(cls.ipa),
166+
doc=cls.doc,
112167
**pragmas,
113168
)
114169

swift/codegen/lib/ql.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ class Property:
3838
is_predicate: bool = False
3939
prev_child: Optional[str] = None
4040
qltest_skip: bool = False
41+
description: List[str] = field(default_factory=list)
42+
doc: Optional[str] = None
43+
doc_plural: Optional[str] = None
4144

4245
def __post_init__(self):
4346
if self.tableparams:
@@ -70,6 +73,10 @@ def is_single(self):
7073
def is_child(self):
7174
return self.prev_child is not None
7275

76+
@property
77+
def has_description(self) -> bool:
78+
return bool(self.description)
79+
7380

7481
@dataclass
7582
class Base:
@@ -94,6 +101,7 @@ class Class:
94101
qltest_collapse_hierarchy: bool = False
95102
qltest_uncollapse_hierarchy: bool = False
96103
ipa: bool = False
104+
doc: List[str] = field(default_factory=list)
97105

98106
def __post_init__(self):
99107
self.bases = [Base(str(b), str(prev)) for b, prev in zip(self.bases, itertools.chain([""], self.bases))]
@@ -120,6 +128,10 @@ def has_children(self) -> bool:
120128
def last_base(self) -> str:
121129
return self.bases[-1].base if self.bases else ""
122130

131+
@property
132+
def has_doc(self) -> bool:
133+
return bool(self.doc)
134+
123135

124136
@dataclass
125137
class Stub:

swift/codegen/lib/schema/defs.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import Callable as _Callable, Union as _Union
2-
from functools import singledispatch as _singledispatch
1+
from typing import Callable as _Callable
32
from swift.codegen.lib import schema as _schema
43
import inspect as _inspect
4+
from dataclasses import dataclass as _dataclass
55

66

77
class _ChildModifier(_schema.PropertyModifier):
@@ -11,30 +11,45 @@ def modify(self, prop: _schema.Property):
1111
prop.is_child = True
1212

1313

14+
@_dataclass
15+
class _DocModifier(_schema.PropertyModifier):
16+
doc: str
17+
18+
def modify(self, prop: _schema.Property):
19+
prop.doc = self.doc
20+
21+
22+
@_dataclass
23+
class _DescModifier(_schema.PropertyModifier):
24+
description: str
25+
26+
def modify(self, prop: _schema.Property):
27+
prop.description = _schema.split_doc(self.description)
28+
29+
1430
def include(source: str):
1531
# add to `includes` variable in calling context
1632
_inspect.currentframe().f_back.f_locals.setdefault(
1733
"__includes", []).append(source)
1834

1935

36+
@_dataclass
2037
class _Pragma(_schema.PropertyModifier):
2138
""" A class or property pragma.
2239
For properties, it functions similarly to a `_PropertyModifier` with `|`, adding the pragma.
2340
For schema classes it acts as a python decorator with `@`.
2441
"""
25-
26-
def __init__(self, pragma):
27-
self.pragma = pragma
42+
pragma: str
2843

2944
def modify(self, prop: _schema.Property):
3045
prop.pragmas.append(self.pragma)
3146

3247
def __call__(self, cls: type) -> type:
3348
""" use this pragma as a decorator on classes """
34-
if "pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas
35-
cls.pragmas.append(self.pragma)
49+
if "_pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas
50+
cls._pragmas.append(self.pragma)
3651
else:
37-
cls.pragmas = [self.pragma]
52+
cls._pragmas = [self.pragma]
3853
return cls
3954

4055

@@ -82,7 +97,7 @@ def __init__(self, **kwargs):
8297
def _annotate(**kwargs) -> _ClassDecorator:
8398
def f(cls: type) -> type:
8499
for k, v in kwargs.items():
85-
setattr(cls, k, v)
100+
setattr(cls, f"_{k}", v)
86101
return cls
87102

88103
return f
@@ -97,13 +112,19 @@ def f(cls: type) -> type:
97112
list = _TypeModifier(_Listifier())
98113

99114
child = _ChildModifier()
115+
doc = _DocModifier
116+
desc = _DescModifier
100117

101118
qltest = _Namespace(
102119
skip=_Pragma("qltest_skip"),
103120
collapse_hierarchy=_Pragma("qltest_collapse_hierarchy"),
104121
uncollapse_hierarchy=_Pragma("qltest_uncollapse_hierarchy"),
105122
)
106123

124+
ql = _Namespace(
125+
default_doc_name=lambda doc: _annotate(doc_name=doc)
126+
)
127+
107128
cpp = _Namespace(
108129
skip=_Pragma("cpp_skip"),
109130
)

swift/codegen/lib/schema/schema.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
""" schema.yml format representation """
22
import pathlib
3+
import re
34
import types
45
import typing
56
from dataclasses import dataclass, field
@@ -35,6 +36,8 @@ class Kind(Enum):
3536
type: Optional[str] = None
3637
is_child: bool = False
3738
pragmas: List[str] = field(default_factory=list)
39+
doc: Optional[str] = None
40+
description: List[str] = field(default_factory=list)
3841

3942
@property
4043
def is_single(self) -> bool:
@@ -76,6 +79,8 @@ class Class:
7679
group: str = ""
7780
pragmas: List[str] = field(default_factory=list)
7881
ipa: Optional[IpaInfo] = None
82+
doc: List[str] = field(default_factory=list)
83+
default_doc_name: Optional[str] = None
7984

8085
@property
8186
def final(self):
@@ -154,6 +159,27 @@ def modify(self, prop: Property):
154159
raise NotImplementedError
155160

156161

162+
def split_doc(doc):
163+
# implementation inspired from https://peps.python.org/pep-0257/
164+
if not doc:
165+
return []
166+
lines = doc.splitlines()
167+
# Determine minimum indentation (first line doesn't count):
168+
strippedlines = (line.lstrip() for line in lines[1:])
169+
indents = [len(line) - len(stripped) for line, stripped in zip(lines[1:], strippedlines) if stripped]
170+
# Remove indentation (first line is special):
171+
trimmed = [lines[0].strip()]
172+
if indents:
173+
indent = min(indents)
174+
trimmed.extend(line[indent:].rstrip() for line in lines[1:])
175+
# Strip off trailing and leading blank lines:
176+
while trimmed and not trimmed[-1]:
177+
trimmed.pop()
178+
while trimmed and not trimmed[0]:
179+
trimmed.pop(0)
180+
return trimmed
181+
182+
157183
@dataclass
158184
class _PropertyNamer(PropertyModifier):
159185
name: str
@@ -167,20 +193,22 @@ def _get_class(cls: type) -> Class:
167193
raise Error(f"Only class definitions allowed in schema, found {cls}")
168194
if cls.__name__[0].islower():
169195
raise Error(f"Class name must be capitalized, found {cls.__name__}")
170-
if len({b.group for b in cls.__bases__ if hasattr(b, "group")}) > 1:
196+
if len({b._group for b in cls.__bases__ if hasattr(b, "_group")}) > 1:
171197
raise Error(f"Bases with mixed groups for {cls.__name__}")
172198
return Class(name=cls.__name__,
173199
bases=[b.__name__ for b in cls.__bases__ if b is not object],
174200
derived={d.__name__ for d in cls.__subclasses__()},
175201
# getattr to inherit from bases
176-
group=getattr(cls, "group", ""),
202+
group=getattr(cls, "_group", ""),
177203
# in the following we don't use `getattr` to avoid inheriting
178-
pragmas=cls.__dict__.get("pragmas", []),
179-
ipa=cls.__dict__.get("ipa", None),
204+
pragmas=cls.__dict__.get("_pragmas", []),
205+
ipa=cls.__dict__.get("_ipa", None),
180206
properties=[
181207
a | _PropertyNamer(n)
182208
for n, a in cls.__dict__.get("__annotations__", {}).items()
183209
],
210+
doc=split_doc(cls.__doc__),
211+
default_doc_name=cls.__dict__.get("_doc_name"),
184212
)
185213

186214

0 commit comments

Comments
 (0)