Created
July 29, 2011 14:07
-
-
Save jrbasso/1113866 to your computer and use it in GitHub Desktop.
API Doc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
oh, I didn't notice api-doc branch. Nice work!