Skip to content

Commit

Permalink
Build out an annotation toolchain
Browse files Browse the repository at this point in the history
  • Loading branch information
jeskew committed Sep 12, 2015
1 parent af907dc commit 05a85e5
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 28 deletions.
8 changes: 8 additions & 0 deletions Makefile
Expand Up @@ -92,6 +92,14 @@ compile-json:
php -dopcache.enable_cli=1 build/compile-json.php
git diff --name-only | grep ^src/data/.*\.json\.php$ || true

annotate-clients: clean
php -dopcache.enable_cli=1 build/annotate-clients.php --tag=latest

annotate-client-locator: clean
php -dopcache.enable_cli=1 build/annotate-client-locator.php

build: compile-json annotate-clients annotate-client-locator package

# Ensures that the TAG variable was passed to the make command
check-tag:
$(if $(TAG),,$(error TAG is not defined. Pass via "make tag TAG=4.2.1"))
Expand Down
114 changes: 114 additions & 0 deletions build/ClassAnnotationUpdater.php
@@ -0,0 +1,114 @@
<?php

/**
* Adds and removes annotations to a class.
*
* @internal
*/
class ClassAnnotationUpdater
{
use PhpFileLinterTrait;

/** @var ReflectionClass */
private $reflection;
/** @var string[] */
private $linesToAppend;
/** @var string */
private $defaultDocBlock;
/** @var string */
private $removeMatching;

public function __construct(
ReflectionClass $reflection,
array $linesToAppend,
$defaultDocBlock,
$removeMatching = ''
) {
$this->reflection = $reflection;
$this->linesToAppend = $linesToAppend;
$this->defaultDocBlock = $defaultDocBlock;
$this->removeMatching = $removeMatching;
}

public function update()
{
// copy the code into memory
$backup = file($this->reflection->getFileName());

list($preamble, $class) = $this->splitClassFile($backup);
$preamble = $this->stripOutExistingDocBlock($preamble);
$preamble .= $this->buildUpdatedDocBlock();

if ($this->writeClassFile(implode(PHP_EOL, [$preamble, $class]))
&& $this->lintFile($this->reflection->getFileName())
) {
return true;
}

$this->writeClassFile(implode('', $backup));
return false;
}

private function splitClassFile(array $lines)
{
$classLineOffset = $this->reflection->getStartLine() - 1;
return [
implode('', array_slice($lines, 0, $classLineOffset)),
implode('', array_slice($lines, $classLineOffset)),
];
}

private function stripOutExistingDocBlock($preamble)
{
if ($this->reflection->getDocComment()) {
return str_replace(
$this->reflection->getDocComment() . PHP_EOL,
'',
$preamble
);
}

return $preamble;
}

private function buildUpdatedDocBlock()
{
$docBlockLines = explode(
PHP_EOL,
$this->reflection->getDocComment() ?: $this->defaultDocBlock
);

// remove lines matching exclusion patterns
if ($this->removeMatching) {
$docBlockLines = array_filter($docBlockLines, function ($line) {
return !preg_match($this->removeMatching, trim($line));
});
}

// hold on to the closing line
$lastLine = array_pop($docBlockLines);

// add a padding line if needed
if (' *' !== end($docBlockLines)) {
$docLines []= ' *';
}

// append API @method annotations
$docBlockLines = array_merge($docBlockLines, $this->linesToAppend);

// add back the closing line
$docBlockLines []= $lastLine;

// send everything back as a string
return implode(PHP_EOL, $docBlockLines);
}

private function writeClassFile($contents)
{
return (bool) file_put_contents(
$this->reflection->getFileName(),
$contents,
LOCK_EX
);
}
}
122 changes: 122 additions & 0 deletions build/ClientAnnotator.php
@@ -0,0 +1,122 @@
<?php

use Aws\Api\ApiProvider;

class ClientAnnotator
{
use PhpFileLinterTrait;

/** @var ReflectionClass */
private $reflection;

public function __construct($clientClassName)
{
$this->reflection = new ReflectionClass($clientClassName);
}

public function updateApiMethodAnnotations()
{
return (new ClassAnnotationUpdater(
$this->reflection,
$this->getMethodAnnotations(),
$this->getDefaultDocComment(),
'/^\* @method \\\\Aws\\\\Result /'
))
->update();
}

private function getMethodAnnotations()
{
$annotations = [];

foreach ($this->getMethods() as $method => $apiVersions) {
$signature = lcfirst($method) . '(array $args = [])';
$annotation = " * @method \\Aws\\Result $signature";

if ($apiVersions !== $this->getVersions()) {
$supportedIn = implode(', ', $apiVersions);
$annotation .= " (supported in versions $supportedIn)";
}

$annotations []= $annotation;
}

return $annotations;
}

private function getMethods()
{
static $methods;

if (empty($methods)) {
$methods = [];
$provider = ApiProvider::defaultProvider();

foreach ($this->getVersions() as $version) {
$methodsInVersion = array_keys(
$provider('api', $this->getEndpoint(), $version)['operations']
);
foreach ($methodsInVersion as $method) {
if (empty($methods[$method])) {
$methods[$method] = [];
}

$methods[$method] []= $version;
}
}
}

return $methods;
}

private function getVersions()
{
static $versions;

if (empty($versions)) {
$versions = ApiProvider::defaultProvider()
->getVersions($this->getEndpoint());

// ensure that versions are always iterated from oldest to newest
sort($versions);
}

return $versions;
}

private function getApiDefinition()
{
static $api;

if (empty($api)) {
$provider = ApiProvider::defaultProvider();
$api = $provider('api', $this->getEndpoint(), 'latest');
}

return $api;
}

private function getEndpoint()
{
static $endpoint;

if (empty($endpoint)) {
$service = strtolower(
preg_replace('/Client$/', '', $this->reflection->getShortName())
);

$endpoint = Aws\manifest($service)['endpoint'];
}

return $endpoint;
}

private function getDefaultDocComment()
{
return <<<EODC
/**
* This client is used to interact with the **{$this->getApiDefinition()['metadata']['serviceFullName']}** service.
*/
EODC;
}
}
24 changes: 3 additions & 21 deletions build/JsonCompiler.php
Expand Up @@ -2,10 +2,10 @@

class JsonCompiler
{
use PhpFileLinterTrait;

/** @var string */
private $path;
/** @var callable */
private $linter;

public function __construct($path)
{
Expand All @@ -14,17 +14,14 @@ public function __construct($path)
}

$this->path = realpath($path);
$this->linter = !empty(opcache_get_status(false)['opcache_enabled']) ?
[$this, 'opcacheLint']
: [$this, 'commandLineLint'];
}

public function compile($outputPath)
{
$backup = $this->readPhpFile($outputPath);

$this->writeFile($outputPath, $this->getTranspiledPhp());
if (!call_user_func($this->linter, $outputPath)) {
if (!$this->lintFile($outputPath)) {
$this->writeFile($outputPath, $backup);
trigger_error(
"Unable to compile {$this->path} to valid PHP",
Expand Down Expand Up @@ -83,19 +80,4 @@ private function writeFile($path, $contents)
{
return file_put_contents($path, $contents, LOCK_EX);
}

private function commandLineLint($path)
{
list($output, $exitCode) = [[], 1];
exec("php -l $path", $output, $exitCode);

return 0 === $exitCode;
}

private function opcacheLint($path)
{
opcache_invalidate($path, true);

return @opcache_compile_file($path);
}
}
55 changes: 55 additions & 0 deletions build/PhpFileLinterTrait.php
@@ -0,0 +1,55 @@
<?php

/**
* A trait that provides a method for linting a PHP file. It will use
* `opcache_compile` if available and fall back to shelling out to `php -l`
* otherwise.
*
* @internal
*/
trait PhpFileLinterTrait
{

/**
* @param string $path
*
* @return bool
*/
private function lintFile($path)
{
static $linter;

if (empty($linter)) {
$linter = !empty(opcache_get_status(false)['opcache_enabled'])
? [$this, 'opcacheLint']
: [$this, 'commandLineLint'];
}

return call_user_func($linter, $path);
}

/**
* @param string $path
*
* @return bool
*/
private function commandLineLint($path)
{
list($output, $exitCode) = [[], 1];
exec("php -l $path", $output, $exitCode);

return 0 === $exitCode;
}

/**
* @param string $path
*
* @return bool
*/
private function opcacheLint($path)
{
opcache_invalidate($path, true);

return @opcache_compile_file($path);
}
}
28 changes: 28 additions & 0 deletions build/annotate-client-locator.php
@@ -0,0 +1,28 @@
<?php
/**
* This file updates the '@method' annotations on the Aws\Sdk class.
*/

require __DIR__ . '/../vendor/autoload.php';

$namespaces = array_map(function (array $manifest) {
return $manifest['namespace'];
}, array_values(Aws\manifest()));

sort($namespaces);

$annotations = array_map(function ($namespace) {
return " * @method \\Aws\\{$namespace}\\{$namespace}Client"
. " create{$namespace}(array \$args = [])";
}, $namespaces);
$previousAnnotationPattern = '/^\* @method'
. ' \\\\Aws\\\\(?:[a-zA-Z0-9]+)\\\\(?:[a-zA-Z0-9]+)Client'
. ' create(?:[a-zA-Z0-9]+)\\(array \$args = \\[\\]\\)/';

(new ClassAnnotationUpdater(
new ReflectionClass(\Aws\Sdk::class),
$annotations,
'',
$previousAnnotationPattern
))
->update();

0 comments on commit 05a85e5

Please sign in to comment.