Skip to content

Instantly share code, notes, and snippets.

@jrbasso
Created July 29, 2011 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrbasso/1113866 to your computer and use it in GitHub Desktop.
Save jrbasso/1113866 to your computer and use it in GitHub Desktop.
API Doc
<?php
// Fake methods
function pluginSplit() {}
function env() {}
class ValidateAPI {
protected $_cakePath;
protected $_files = array();
protected $_cakeClasses;
protected $_messages = array();
protected $_allowedTypes = array('string', 'integer', 'float', 'mixed', 'void', 'array', 'boolean', 'resource', 'object', '[A-Z]\w+', 'stdClass');
public function __construct($cakePath = null) {
if ($cakePath) {
$this->_cakePath = $cakePath;
} else {
$this->_cakePath = getcwd() . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'Cake';
}
if (!is_file($this->_cakePath . DIRECTORY_SEPARATOR . 'basics.php')) {
throw new Exception('Invalid cake folder.');
}
}
public function prepare() {
spl_autoload_register(array($this, 'autoload'));
error_reporting(error_reporting() | E_STRICT);
set_error_handler(array($this, 'errorHandler'));
$this->cakeDefinitions();
return $this;
}
public function autoload($name) {
// Hard-coded the classes that is not a filename
if ($name === 'CacheEngine') {
return include_once $this->_cakePath . DIRECTORY_SEPARATOR . 'Cache' . DIRECTORY_SEPARATOR . 'Cache.php';
}
if ($name === 'CakeSessionHandlerInterface') {
return include_once $this->_cakePath . DIRECTORY_SEPARATOR . 'Model' . DIRECTORY_SEPARATOR . 'Datasource' . DIRECTORY_SEPARATOR . 'CakeSession.php';
}
foreach ($this->_files as $file) {
if (preg_match("/[\/\\\]{$name}\.php$/", $file)) {
return include_once $file;
}
}
return false;
}
public function errorHandler($errno, $errstr, $errfile, $errline) {
if (!(error_reporting() & $errno)) {
return;
}
switch ($errno) {
case E_USER_ERROR:
$this->out('Fatal: ' . $errstr);
exit(1);
case E_STRICT:
$this->out('Strict: ' . $errstr);
break;
default:
$this->out('Other error: ' . $errstr);
}
return true;
}
public function run() {
$this->_messages = array();
$this->_files = $this->_fileList($this->_cakePath);
$nativeClasses = get_declared_classes();
$this->_loadFiles();
$this->_cakeClasses = array_diff(get_declared_classes(), $nativeClasses);
$this->_processClasses();
$this->_processFiles();
return $this;
}
public function outputMessages() {
foreach ($this->_messages as $message) {
$this->out($message);
}
}
protected function cakeDefinitions() {
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(dirname($this->_cakePath)));
define('APP_DIR', '');
define('CAKE_CORE_INCLUDE_PATH', dirname($this->_cakePath));
define('WEBROOT_DIR', '');
define('WWW_ROOT', '');
define('APP_PATH', '');
define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS);
define('LOG_ERROR', 0);
}
protected function _fileList($path) {
$files = array();
$folder = new DirectoryIterator($path);
foreach ($folder as $info) {
if ($this->_isValidFile($info->getPathname())) {
$files[] = $info->getPathname();
} elseif ($info->isDir() && !$info->isDot()) {
$files = array_merge($files, $this->_fileList($info->getPathname()));
}
}
return $files;
}
protected function _loadFiles() {
foreach ($this->_files as $file) {
include_once $file;
}
}
protected function _isValidFile($file) {
if (
is_file($file) &&
pathinfo($file, PATHINFO_EXTENSION) === 'php' &&
strpos($file, 'Cake' . DS . 'Test' . DS) === false &&
strpos($file, 'Cake' . DS . 'TestSuite' . DS) === false &&
strpos($file, 'Cake' . DS . 'Console' . DS . 'Templates' . DS) === false &&
preg_match('/^[A-Z]/', pathinfo($file, PATHINFO_FILENAME))
) {
return true;
}
return false;
}
protected function _processClasses() {
foreach ($this->_cakeClasses as $cakeClass) {
$this->_processClass($cakeClass);
}
}
protected function _processClass($class) {
$refClass = new ReflectionClass($class);
$this->_checkClassPackage($refClass);
foreach ($refClass->getProperties() as $refProperty) {
if ($refProperty->class !== $class) {
continue;
}
$this->_processProperty($refProperty);
}
foreach ($refClass->getMethods() as $refMethod) {
if ($refMethod->class !== $class) {
continue;
}
$this->_processMethod($refMethod);
}
}
protected function _checkClassPackage($refClass) {
if (strpos($refClass->getDocComment(), '* @package') === false) {
$this->_messages[] = "Missing @package information on '{$refClass->name}'...";
}
}
protected function _processProperty($refProperty) {
$this->_checkPropertyVar($refProperty);
$this->_checkPropertyAccess($refProperty);
}
protected function _checkPropertyVar($refProperty) {
$doc = $refProperty->getDocComment();
// Test missing @var
if (!preg_match('/ * @var (\w+)/', $doc, $matches)) {
$this->_messages[] = "Missing @var on '{$refProperty->class}::\${$refProperty->name}'...";
return;
}
// Check if the type in in allowed list
$pattern = implode('|', $this->_allowedTypes);
$pattern = '#^(' . $pattern . ')(\|(' . $pattern . '))*$#';
if (!preg_match($pattern, $matches[1])) {
$this->_messages[] = "Property of '{$refProperty->class}::\${$refProperty->name}' ({$matches[1]}) is not allowed...";
}
}
protected function _checkPropertyAccess($refProperty) {
if (strpos($refProperty->getDocComment(), '@access') !== false) {
$this->_messages[] = "Found @access on '{$refProperty->class}::\${$refProperty->name}'...";
}
}
protected function _processMethod($refMethod) {
$this->_checkMethodParams($refMethod);
$this->_checkMethodReturn($refMethod);
$this->_checkMethodAccess($refMethod);
$this->_checkMethodStatic($refMethod);
$this->_checkMethodException($refMethod);
$this->_checkMethodVisibility($refMethod);
}
protected function _checkMethodParams($refMethod) {
$docParameters = array();
if (preg_match_all('/ * @param ([\w|]+) \$(\w+)/i', $refMethod->getDocComment(), $matches)) {
$docParameters = array_combine($matches[2], $matches[1]);
}
$methodParameters = array();
foreach ($refMethod->getParameters() as $refParameter) {
$methodParameters[$refParameter->name] = null;
if (preg_match('/\[ \<\w+\> (\w+) \$\w+ \]/', (string)$refParameter, $matches)) {
$methodParameters[$refParameter->name] = $matches[1];
}
}
// Test if method are in docs
foreach ($methodParameters as $param => $type) {
if (!isset($docParameters[$param])) {
$this->_messages[] = "Doc for parameter \${$param} not found on '{$refMethod->class}::{$refMethod->name}()'...\n";
}
}
// Test if parameters of doc exists and are in the same order of method
if (array_keys($docParameters) !== array_keys($methodParameters)) {
$this->_messages[] = "Invalid number of parameters on '{$refMethod->class}::{$refMethod->name}()'...\n" .
"\tPHP Parameters: " . implode(", ", array_keys($methodParameters)) . "\n" .
"\tDoc Parameters: " . implode(", ", array_keys($docParameters));
}
// Check code defined types
foreach ($methodParameters as $param => $type) {
if ($type !== null && isset($docParameters[$param]) && $docParameters[$param] !== $type) {
$this->_messages[] = "Invalid type on parameter {$param} to '{$refMethod->class}::{$refMethod->name}()'... Must be {$type} (defined {$docParameters[$param]}).";
}
}
// Check if the types are in allowed list
$pattern = implode('|', $this->_allowedTypes);
$pattern = '#^(' . $pattern . ')(\|(' . $pattern . '))*$#';
foreach ($docParameters as $param => $type) {
if (!preg_match($pattern, $type)) {
$this->_messages[] = "Param '{$type}' is not allowed to parameter '{$param}' on '{$refMethod->class}::{$refMethod->name}()'...";
}
}
}
protected function _checkMethodReturn($refMethod) {
$doc = $refMethod->getDocComment();
// Test return on constructor
if ($refMethod->isConstructor() || $refMethod->isDestructor()) {
if (strpos($doc, '@return') !== false) {
$this->_messages[] = "Constructor and destructor must not have @return on '{$refMethod->class}::{$refMethod->name}()'...";
}
return;
}
// Test return on methods
if (!preg_match('/ * @return (\w+)/i', $doc, $matches)) {
$this->_messages[] = "Missing @return information on '{$refMethod->class}::{$refMethod->name}()'...";
return;
}
// Check if the type in in allowed list
$pattern = implode('|', $this->_allowedTypes);
$pattern = '#^(' . $pattern . ')(\|(' . $pattern . '))*$#';
if (!preg_match($pattern, $matches[1])) {
$this->_messages[] = "Return of '{$refMethod->class}::{$refMethod->name}()' ({$matches[1]}) is not allowed...";
}
}
protected function _checkMethodAccess($refMethod) {
if (strpos($refMethod->getDocComment(), '@access') !== false) {
$this->_messages[] = "Found @access on '{$refMethod->class}::{$refMethod->name}()'...";
}
}
protected function _checkMethodStatic($refMethod) {
if (strpos($refMethod->getDocComment(), '@static') !== false) {
$this->_messages[] = "Found @access on '{$refMethod->class}::{$refMethod->name}()'...";
}
}
protected function _checkMethodException($refMethod) {
$length = $refMethod->getEndLine() - $refMethod->getStartLine();
$source = file($refMethod->getFileName());
$source = implode('', array_slice($source, $refMethod->getStartLine(), $length));
$throws = array();
if (preg_match_all('/throw new (\w+) *\(/i', $source, $matches)) {
$throws = array_unique($matches[1]);
$doc = $refMethod->getDocComment();
$throwsPos = strpos($doc, '@throws');
if ($throwsPos === false) {
$this->_messages[] = "Missing @throws on '{$refMethod->class}::{$refMethod->name}()'... Exceptions: " . implode(', ', $throws);
} else {
$throwsLine = substr($doc, $throwsPos, strpos($doc, "\n", $throwsPos) - $throwsPos);
foreach ($throws as $throw) {
if (strpos($throwsLine, $throw) === false) {
$this->_messages[] = "Missing {$throw} on @throws of '{$refMethod->class}::{$refMethod->name}()'...";
}
}
}
}
}
protected function _checkMethodVisibility($refMethod) {
$source = file($refMethod->getFileName());
$source = $source[$refMethod->getStartLine() - 1];
if (!preg_match("/^\s*(?:abstract )?(public|protected|private)( static)? function &?{$refMethod->name}\(/", $source, $matches)) {
$this->_messages[] = "Missing visibility to '{$refMethod->class}::{$refMethod->name}()'...";
return;
}
if (substr($refMethod->name, 0, 2) === '__') {
if ($matches[1] !== 'private' && !in_array($refMethod->name, array('__call', '__isset', '__get', '__set', '__construct', '__destruct', '__toString'))) {
$this->_messages[] = "Method '{$refMethod->class}::{$refMethod->name}()' must be private (now is {$matches[1]})...";
}
} elseif ($refMethod->name[0] === '_') {
if ($matches[1] !== 'protected') {
$this->_messages[] = "Method '{$refMethod->class}::{$refMethod->name}()' must be protected (now is {$matches[1]})...";
}
} elseif ($matches[1] !== 'public') {
$this->_messages[] = "Method '{$refMethod->class}::{$refMethod->name}()' must be public (now is {$matches[1]})...";
}
}
protected function _processFiles() {
foreach ($this->_files as $file) {
$content = file_get_contents($file);
$this->_checkFileLicense($file, $content);
$this->_checkFileCopyright($file, $content);
$this->_checkFileTrailingSpaces($file, $content);
}
}
protected function _checkFileLicense($file, &$content) {
$pattern = '|\* @license +MIT License \(http://www.opensource.org/licenses/mit-license\.php\)|';
if (!preg_match($pattern, $content)) {
$this->_messages[] = "Invalid license on {$file}...";
}
}
protected function _checkFileCopyright($file, &$content) {
$pattern = '|\* @copyright +Copyright 2005-2011, Cake Software Foundation, Inc\. \(http://cakefoundation\.org\)|';
if (!preg_match($pattern, $content)) {
$this->_messages[] = "Invalid copyright on {$file}...";
}
}
protected function _checkFileTrailingSpaces($file, &$content) {
foreach (explode("\n", $content) as $i => $line) {
if (preg_match('/\s+$/', $line)) {
$i++;
$this->_messages[] = "Trailing space on {$file} line {$i}...";
}
}
}
protected function out($string, $newLine = true) {
echo $string;
if ($newLine) {
echo "\n";
}
}
}
$validator = new ValidateAPI(isset($argv[1]) ? $argv[1] : null);
$validator->prepare()->run()->outputMessages();
@majna
Copy link

majna commented Aug 21, 2011

oh, I didn't notice api-doc branch. Nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment