Skip to content

Commit 6d20695

Browse files
authored
Add support for codegen xhp classes (#138)
* Add support for codegen xhp classes Adds xhp attribute codegen to traits and classes. No error is thrown when attributes are added to non-xhp classes. This syntax is valid, but not useful in the current class. Inheritance maybe? * Fix broken copy paste * Compatibility with older hsl versions * Whitespace lint * Update xhp attribute doc I copy pasted property and did not touch up the doc well enough.
1 parent 05c98f4 commit 6d20695

10 files changed

+291
-5
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "facebook/hack-codegen",
3-
"description": "Hack Codegen is a library for programatically generating Hack code",
3+
"description": "Hack Codegen is a library for programmatically generating Hack code",
44
"keywords": ["code generation", "Hack"],
55
"require": {
66
"hhvm": "^4.80",

src/CodegenClass.hack

+10-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ final class CodegenClass extends CodegenClassish {
3232
private string $declComment = '';
3333
private bool $isFinal = false;
3434
private bool $isAbstract = false;
35+
private bool $isXHP = false;
3536
private ?CodegenConstructor $constructor = null;
3637

3738
/** @selfdocumenting */
@@ -46,6 +47,12 @@ final class CodegenClass extends CodegenClassish {
4647
return $this;
4748
}
4849

50+
/** @selfdocumenting */
51+
public function setIsXHP(bool $value = true): this {
52+
$this->isXHP = $value;
53+
return $this;
54+
}
55+
4956
/** Set the parent class of the generated class. */
5057
public function setExtends(string $name): this {
5158
return $this->setExtendsf('%s', $name);
@@ -148,10 +155,11 @@ final class CodegenClass extends CodegenClassish {
148155
$generics_dec = $this->buildGenericsDeclaration();
149156

150157
$builder->addWithSuggestedLineBreaksf(
151-
'%s%s%s%s%s',
158+
'%s%s%s%s%s%s',
152159
$this->declComment,
153160
$this->isAbstract ? 'abstract ' : '',
154161
$this->isFinal ? 'final ' : '',
162+
$this->isXHP ? 'xhp ' : '',
155163
'class '.$this->name.$generics_dec,
156164
$this->extendsClass !== null
157165
? HackBuilder::DELIMITER.'extends '.$this->extendsClass
@@ -172,6 +180,7 @@ final class CodegenClass extends CodegenClassish {
172180
protected function appendBodyToBuilder(HackBuilder $builder): void {
173181
$this->buildTraits($builder);
174182
$this->buildConsts($builder);
183+
$this->buildXHPAttributes($builder);
175184
$this->buildVars($builder);
176185
$this->buildManualDeclarations($builder);
177186
$this->buildConstructor($builder);

src/CodegenClassish.hack

+33-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
namespace Facebook\HackCodegen;
1111

1212
use namespace HH\Lib\{C, Str, Vec};
13-
use namespace Facebook\HackCodegen\_Private\C as CP;
13+
use namespace Facebook\HackCodegen\_Private\{C as CP, Vec as VecP};
1414

1515
/**
1616
* Abstract class to generate class-like definitions.
@@ -33,6 +33,7 @@ abstract class CodegenClassish implements ICodeBuilderRenderer {
3333
protected vec<CodegenMethod> $methods = vec[];
3434
private vec<CodegenUsesTrait> $traits = vec[];
3535
protected vec<CodegenConstantish> $consts = vec[];
36+
protected vec<CodegenXHPAttribute> $xhpAttributes = vec[];
3637
protected vec<CodegenProperty> $vars = vec[];
3738
private bool $isConsistentConstruct = false;
3839
protected bool $hasManualFooter = false;
@@ -153,6 +154,20 @@ abstract class CodegenClassish implements ICodeBuilderRenderer {
153154
return $this;
154155
}
155156

157+
/** @selfdocumenting */
158+
public function addXhpAttribute(CodegenXHPAttribute $attribute): this {
159+
$this->xhpAttributes[] = $attribute;
160+
return $this;
161+
}
162+
163+
/** @selfdocumenting */
164+
public function addXhpAttributes(Traversable<CodegenXHPAttribute> $attributes): this {
165+
foreach($attributes as $attr) {
166+
$this->addXhpAttribute($attr);
167+
}
168+
return $this;
169+
}
170+
156171
/** @selfdocumenting */
157172
public function setDocBlock(string $comment): this {
158173
$this->docBlock = $comment;
@@ -315,6 +330,22 @@ abstract class CodegenClassish implements ICodeBuilderRenderer {
315330
}
316331
}
317332

333+
protected function buildXHPAttributes(HackBuilder $builder): void {
334+
if (C\is_empty($this->xhpAttributes)) {
335+
return;
336+
}
337+
$builder->ensureNewLine();
338+
$builder->addLine('attribute')->indent();
339+
340+
$attributes = $this->xhpAttributes;
341+
$last = VecP\pop_backx(inout $attributes);
342+
foreach($attributes as $attr) {
343+
$builder->addRenderer($attr);
344+
$builder->addLine(',');
345+
}
346+
$builder->addRenderer($last)->addLine(';')->unindent();
347+
}
348+
318349
protected function buildVars(HackBuilder $builder): void {
319350
if (C\is_empty($this->vars)) {
320351
return;
@@ -380,7 +411,7 @@ abstract class CodegenClassish implements ICodeBuilderRenderer {
380411
$generated_from =
381412
$this->generatedFrom ? $this->generatedFrom->render() : null;
382413

383-
$doc_block_parts = \array_filter(varray[$this->docBlock, $generated_from]);
414+
$doc_block_parts = Vec\filter_nulls(vec[$this->docBlock, $generated_from]);
384415

385416
if ($doc_block_parts) {
386417
$builder->addDocBlock(Str\join($doc_block_parts, "\n\n"));

src/CodegenFactoryTrait.hack

+11
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,15 @@ trait CodegenFactoryTrait implements ICodegenFactory {
229229
final public function codegenNewtype(string $name): CodegenType {
230230
return (new CodegenType($this->getConfig(), $name))->newType();
231231
}
232+
233+
final public function codegenXHPAttribute(string $name): CodegenXHPAttribute {
234+
return new CodegenXHPAttribute($this->getConfig(), $name);
235+
}
236+
237+
final public function codegenXHPAttributef(
238+
Str\SprintfFormatString $format,
239+
mixed ...$args
240+
): CodegenXHPAttribute {
241+
return $this->codegenXHPAttribute(\vsprintf($format, $args));
242+
}
232243
}

src/CodegenTrait.hack

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ final class CodegenTrait extends CodegenClassish {
6363
$this->buildRequires($builder);
6464
$this->buildTraits($builder);
6565
$this->buildConsts($builder);
66+
$this->buildXHPAttributes($builder);
6667
$this->buildVars($builder);
6768
$this->buildManualDeclarations($builder);
6869
$this->buildMethods($builder);

src/CodegenXHPAttribute.hack

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
*/
9+
10+
namespace Facebook\HackCodegen;
11+
12+
use namespace HH\Lib\Str;
13+
14+
/**
15+
* Generate code for an xhp attribute. Please don't use this class directly;
16+
* instead use the function ICodegenFactory->codegenAttribute. E.g.:
17+
*
18+
* ICodegenFactory->codegenAttribute('src')
19+
* ->setType('string')
20+
* ->setInlineComment('A script src must be a valid URI')
21+
* ->render();
22+
*/
23+
final class CodegenXHPAttribute implements ICodeBuilderRenderer {
24+
25+
use HackBuilderRenderer;
26+
27+
private ?string $comment;
28+
private ?string $type;
29+
private ?string $value;
30+
private ?XHPAttributeDecorator $decorator;
31+
32+
public function __construct(
33+
protected IHackCodegenConfig $config,
34+
private string $name,
35+
) {}
36+
37+
public function getName(): string {
38+
return $this->name;
39+
}
40+
41+
public function getType(): ?string {
42+
return $this->type;
43+
}
44+
45+
public function getValue(): mixed {
46+
return $this->value;
47+
}
48+
49+
public function setDecorator(?XHPAttributeDecorator $decorator): this {
50+
invariant(
51+
$decorator is null || $this->value is null,
52+
'XHP attributes with a default value can not have an %s decorator',
53+
xhp_attribute_decorator_to_string($decorator),
54+
);
55+
$this->decorator = $decorator;
56+
return $this;
57+
}
58+
59+
public function setInlineComment(string $comment): this {
60+
$this->comment = $comment;
61+
return $this;
62+
}
63+
64+
/**
65+
* Set the type of the member var. In Hack, if it's nullable
66+
* you should prepend the question mark, e.g. "?string".
67+
* XHP enums should be avoided, but you can specify "enum { 'foo' }"
68+
* as a literal string if you need it.
69+
*/
70+
public function setType(string $type): this {
71+
$this->type = $type;
72+
return $this;
73+
}
74+
75+
public function setTypef(
76+
Str\SprintfFormatString $format,
77+
mixed ...$args
78+
): this {
79+
return $this->setType(\vsprintf($format, $args));
80+
}
81+
82+
/**
83+
* Set the initial value for the variable. You can pass numbers, strings,
84+
* arrays, etc, and it will generate the code to render those values.
85+
*/
86+
public function setValue<T>(
87+
T $value,
88+
IHackBuilderValueRenderer<T> $renderer,
89+
): this {
90+
invariant(
91+
$this->decorator is null,
92+
'XHP attributes with an %s decorator can not have a default value',
93+
xhp_attribute_decorator_to_string($this->decorator),
94+
);
95+
$this->value = $renderer->render($this->config, $value);
96+
return $this;
97+
}
98+
99+
public function appendToBuilder(HackBuilder $builder): HackBuilder {
100+
$value = $this->value;
101+
102+
return $builder
103+
->addDocBlock($this->comment)
104+
->addIf($this->type is nonnull, $this->type.' ')
105+
->add($this->name)
106+
->addIf($this->value is nonnull, ' = '.$value)
107+
->addIf(
108+
$this->decorator is nonnull,
109+
' '.
110+
xhp_attribute_decorator_to_string(
111+
$this->decorator ?? XHPAttributeDecorator::REQUIRED,
112+
),
113+
);
114+
}
115+
116+
}

src/ICodegenFactory.hack

+19-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ interface ICodegenFactory {
201201
public function codegenProperty(string $name): CodegenProperty;
202202

203203
/**
204-
* Generate a class or trait property, using a %-placehodler format string
204+
* Generate a class or trait property, using a %-placeholder format string
205205
* for the property name.
206206
*
207207
* @see codegenProperty
@@ -307,4 +307,22 @@ interface ICodegenFactory {
307307
* @see codegenType
308308
*/
309309
public function codegenNewtype(string $name): CodegenType;
310+
311+
/**
312+
* Generate a class of trait xhp attribute.
313+
*
314+
* @see codegenXHPAttributef
315+
*/
316+
public function codegenXHPAttribute(string $name): CodegenXHPAttribute;
317+
318+
/**
319+
* Generate a class or trait xhp attribute, using a %-placeholder format
320+
* string for the attribute name.
321+
*
322+
* @see codegenXHPAttribute
323+
*/
324+
public function codegenXHPAttributef(
325+
Str\SprintfFormatString $format,
326+
mixed ...$args
327+
): CodegenXHPAttribute;
310328
}

src/XHPAttributeDecorator.hack

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
*/
9+
10+
namespace Facebook\HackCodegen;
11+
12+
enum XHPAttributeDecorator: int {
13+
REQUIRED = 0;
14+
LATE_INIT = 1;
15+
}
16+
17+
function xhp_attribute_decorator_to_string(
18+
XHPAttributeDecorator $decorator,
19+
): string {
20+
switch ($decorator) {
21+
case XHPAttributeDecorator::REQUIRED:
22+
return '@required';
23+
case XHPAttributeDecorator::LATE_INIT:
24+
return '@lateinit';
25+
}
26+
}

tests/CodegenClassTest.codegen

+17
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,20 @@ class JKRowling
150150
IRonWeasley {
151151
}
152152

153+
!@#$%codegentest:testXHPClassWithAttributes
154+
xhp class a {
155+
156+
const string BETWEEN_CONSTS = '';
157+
attribute
158+
/**
159+
* The web is a magical place where a string with a set structure can be
160+
* the key to visiting some remote place on the internet where you can find
161+
* content made by other people.
162+
*/
163+
string href @required,
164+
string target = 'about:blank',
165+
string hreflang @lateinit;
166+
167+
private null $andProps;
168+
}
169+

0 commit comments

Comments
 (0)