src/CodegenFunctionish.hack (246 lines of code) (raw):
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
namespace Facebook\HackCodegen;
use namespace HH\Lib\{Str, Vec};
/**
* Base class to generate a function or a method.
*/
abstract class CodegenFunctionish implements ICodeBuilderRenderer {
use HackBuilderRenderer;
use CodegenWithAttributes;
protected string $name;
protected ?string $body = null;
protected ?string $docBlock = null;
protected ?keyset<string> $contexts = null;
protected ?string $returnType = null;
private ?string $fixme = null;
protected bool $isAsync = false;
protected bool $isOverride = false;
protected bool $isManualBody = false;
protected bool $isMemoized = false;
protected vec<string> $parameters = vec[];
protected ?CodegenGeneratedFrom $generatedFrom;
public function __construct(
protected IHackCodegenConfig $config,
string $name,
) {
$this->name = $name;
}
public function setName(string $name): this {
$this->name = $name;
return $this;
}
public function setIsAsync(bool $value = true): this {
$this->isAsync = $value;
return $this;
}
public function setIsMemoized(bool $value = true): this {
$this->isMemoized = $value;
return $this;
}
public function addContext(
string $context,
): this {
if($this->contexts === null) {
$this->contexts = keyset<string>[$context];
} else {
$this->contexts[] = $context;
}
return $this;
}
public function addContexts(
Traversable<string> $contexts,
): this {
foreach($contexts as $context) {
$this->addContext($context);
}
return $this;
}
public function setReturnType(string $type): this {
return $this->setReturnTypef('%s', $type);
}
public function setReturnTypef(
Str\SprintfFormatString $type,
mixed ...$args
): this {
$type = \vsprintf($type, $args);
if ($type) {
$this->returnType = $type;
}
return $this;
}
public function addParameter(string $param): this {
return $this->addParameterf('%s', $param);
}
public function addParameterf(
Str\SprintfFormatString $param,
mixed ...$args
): this {
$param = \vsprintf($param, $args);
$this->parameters[] = $param;
return $this;
}
public function addParameters(Traversable<string> $params): this {
foreach ($params as $param) {
$this->addParameter($param);
}
return $this;
}
public function setBody(string $body): this {
return $this->setBodyf('%s', $body);
}
public function setBodyf(
Str\SprintfFormatString $body,
mixed ...$args
): this {
$this->body = \vsprintf($body, $args);
return $this;
}
public function setManualBody(bool $val = true): this {
if ($val) {
if ($this->body === null) {
$this->body = "throw new ViolationException('Unimplemented');";
}
}
$this->isManualBody = $val;
return $this;
}
public function setDocBlock(string $comment): this {
$this->docBlock = $comment;
return $this;
}
public function setGeneratedFrom(CodegenGeneratedFrom $from): this {
$this->generatedFrom = $from;
return $this;
}
public function getName(): string {
return $this->name;
}
public function getParameters(): vec<string> {
return $this->parameters;
}
public function getContexts(): ?keyset<string> {
return $this->contexts;
}
public function getReturnType(): ?string {
return $this->returnType;
}
public function isManualBody(): bool {
return $this->isManualBody;
}
/**
* Break lines for function declaration. First calculate the string length as
* if there were no line break. If the string exceeds one line, try break
* by having each parameter per line.
*
* $is_abstract - only valid for CodegenMethodX for code reuse purposes
*/
protected function getFunctionDeclarationBase(
string $keywords,
bool $is_abstract = false,
): string {
$builder = (new HackBuilder($this->config))
->add($keywords)
->addf('%s(%s)', $this->name, Str\join($this->parameters, ', '))
->addIf($this->contexts !== null, '[' . Str\join($this->contexts ?? keyset[], ', ') . ']')
->addIf($this->returnType !== null, ': '.($this->returnType ?? ''));
$code = $builder->getCode();
// If the total length is longer than max len, try to break it. Otherwise
// return Total length = 2 (indent) + codelength + 2 or 1 (" {" or ";")
// If the function/method is abstract, the ";" will be appended later
// Therefore it has one char less than non-abstract functions, which has "{"
if (
Str\length($code) <=
$this->config->getMaxLineLength() - 4 + (int)$is_abstract ||
$this->fixme !== null
) {
return (new HackBuilder($this->config))->add($code)->getCode();
} else {
$parameter_lines = Vec\map(
$this->parameters,
$line ==> {
if (Str\search($line, '...$') !== null) {
return $line;
}
return $line.',';
},
);
$multi_line_builder = (new HackBuilder($this->config))
->add($keywords)
->addLine($this->name.'(')
->indent()
->addLines($parameter_lines)
->unindent()
->add(')')
->addIf($this->contexts !== null, '[' . Str\join($this->contexts ?? keyset[], ', ') . ']')
->addIf($this->returnType !== null, ': '.($this->returnType ?? ''));
return $multi_line_builder->getCode();
}
}
protected function getMaxCodeLength(): int {
$max_length = $this->config->getMaxLineLength();
if ($this is CodegenMethodish) {
$max_length -= $this->config->getSpacesPerIndentation();
}
return $max_length;
}
public function addHHFixMe(int $code, string $why): this {
$max_length = $this->getMaxCodeLength() - 6;
$str = \sprintf('HH_FIXME[%d] %s', $code, $why);
invariant(
\strlen($str) <= $max_length,
'ERROR: Your fixme has to fit on one line, with indentation '.
'and comments. So you need to shorten your message by %d '.
'characters.',
\strlen($str) - $max_length,
);
$this->fixme = $str;
return $this;
}
/**
* $is_abstract and $containing_class_name
* only valid for CodegenMethodX for code reuse purposes
*/
protected function appendToBuilderBase(
HackBuilder $builder,
string $func_declaration,
bool $is_abstract = false,
string $containing_class_name = '',
): HackBuilder {
if ($this->docBlock !== null && $this->docBlock !== '') {
if ($this->generatedFrom) {
$builder->addDocBlock(
$this->docBlock."\n(".$this->generatedFrom->render().')',
);
} else {
$builder->addDocBlock($this->docBlock);
}
} else {
if ($this->generatedFrom) {
$builder->addInlineComment($this->generatedFrom->render());
}
}
if ($this->hasAttributes()) {
$builder->ensureNewLine()->addLine($this->renderAttributes());
}
if ($this->fixme !== null) {
$builder->addInlineCommentWithStars($this->fixme);
}
$builder->add($func_declaration);
if ($is_abstract) {
$builder->addLine(';');
return $builder;
}
$builder->openBrace();
if ($this->isManualBody) {
$builder->startManualSection($containing_class_name.$this->name);
$builder->add($this->body);
$builder->endManualSection();
} else {
$builder->add($this->body);
}
$builder->closeBrace();
return $builder;
}
protected function getExtraAttributes(): dict<string, vec<string>> {
$attributes = dict[];
if ($this->isOverride) {
$attributes['__Override'] = vec[];
}
if ($this->isMemoized) {
$attributes['__Memoize'] = vec[];
}
return $attributes;
}
private function getFunctionDeclaration(): string {
// $keywords is shared by both single and multi line declaration
$keywords = (new HackBuilder($this->config))
->addIf($this->isAsync, 'async ')
->add('function ')
->getCode();
return $this->getFunctionDeclarationBase($keywords);
}
public function appendToBuilder(HackBuilder $builder): HackBuilder {
$func_declaration = $this->getFunctionDeclaration();
return $this->appendToBuilderBase($builder, $func_declaration);
}
}