diff options
Diffstat (limited to 'www/wiki/tests/phan/bin')
-rwxr-xr-x | www/wiki/tests/phan/bin/phan | 90 | ||||
-rw-r--r-- | www/wiki/tests/phan/bin/postprocess-phan.php | 146 |
2 files changed, 236 insertions, 0 deletions
diff --git a/www/wiki/tests/phan/bin/phan b/www/wiki/tests/phan/bin/phan new file mode 100755 index 00000000..ad06823a --- /dev/null +++ b/www/wiki/tests/phan/bin/phan @@ -0,0 +1,90 @@ +#!/bin/bash + +# mediawiki-vagrant installs dont have realpath by default +if ! which realpath > /dev/null; then + realpath() { + php -r "echo realpath('$*');" + } +fi + +if hash php7.0 2>/dev/null; then + export PHP="php7.0" +else + export PHP="php" +fi + +# Note that this isn't loaded in via composer because then composer can +# only be run with php7.0 +if [ ! -f "$PHAN" ]; then + # If no PHAN is specified then try to get location from PATH + export PHAN="$(which phan)" + if [ ! -f "$PHAN" ]; then + echo "The environment variable PHAN must point to the 'phan' file" + echo "in a checkout of https://github.com/etsy/phan.git" + echo "Or phan must be included in your PATH" + exit 1 + fi +else + export PHAN="$PHP $PHAN" +fi + +if [ -z "$MW_INSTALL_PATH" ]; then + # Figure out where mediawiki is based on the location of this script + pushd "$(dirname "$0")" > /dev/null + export MW_INSTALL_PATH="$(git rev-parse --show-toplevel)" + popd >/dev/null +fi + +# If the first argument doesn't start with a -, then it's a path +# to another project (extension, skin, etc.) to analyze +if [[ -n "$1" && "$1" != "-"* ]]; then + cd $1 + shift +else + cd "$(dirname "$0")" +fi + +# Root directory of project +export ROOT="$(git rev-parse --show-toplevel)" + +# Go to the root of this git repo +cd "$ROOT" + +export CONFIG_FILE="$ROOT/tests/phan/config.php" +if [ ! -f "$CONFIG_FILE" ]; then + echo "Could not find a phan config file to apply in" + echo "$CONFIG_FILE" + exit 1 +fi + +# Phan's issues directory +export ISSUES="${ROOT}/tests/phan/issues" +mkdir -p "$ISSUES" + +# Get the current hash of HEAD +export REV="$(git rev-parse HEAD)" + +# Destination for issues found +export RUN="${ISSUES}/issues-${REV}" + + +# Run the analysis, emitting output to the +# issues file. +$PHAN \ + --project-root-directory "$ROOT" \ + --config-file "$CONFIG_FILE" \ + --output "php://stdout" \ + "${@}" \ + | php "$MW_INSTALL_PATH/tests/phan/bin/postprocess-phan.php" "${@}" \ + > $RUN + +EXIT_CODE="$?" + +# Re-link the latest file +rm -f "${ISSUES}/latest" +ln -s "${RUN}" "${ISSUES}/latest" + +# Output any issues that were found +cat "${RUN}" + +exit $EXIT_CODE diff --git a/www/wiki/tests/phan/bin/postprocess-phan.php b/www/wiki/tests/phan/bin/postprocess-phan.php new file mode 100644 index 00000000..3e805986 --- /dev/null +++ b/www/wiki/tests/phan/bin/postprocess-phan.php @@ -0,0 +1,146 @@ +<?php + +abstract class Suppressor { + /** + * @param string $input + * @return bool do errors remain + */ + abstract public function suppress( $input ); + + /** + * @param string[] $source + * @param string $type + * @param int $lineno + * @return bool + */ + protected function isSuppressed( array $source, $type, $lineno ) { + return $lineno > 0 && preg_match( + "|/\*\* @suppress {$type} |", + $source[$lineno - 1] + ); + } +} + +class TextSuppressor extends Suppressor { + /** + * @param string $input + * @return bool do errors remain + */ + public function suppress( $input ) { + $hasErrors = false; + $errors = []; + foreach ( explode( "\n", $input ) as $error ) { + if ( empty( $error ) ) { + continue; + } + if ( !preg_match( '/^(.*):(\d+) (Phan\w+) (.*)$/', $error, $matches ) ) { + echo "Failed to parse line: $error\n"; + continue; + } + list( $source, $file, $lineno, $type, $message ) = $matches; + $errors[$file][] = [ + 'orig' => $error, + // convert from 1 indexed to 0 indexed + 'lineno' => $lineno - 1, + 'type' => $type, + ]; + } + foreach ( $errors as $file => $fileErrors ) { + $source = file( $file ); + foreach ( $fileErrors as $error ) { + if ( !$this->isSuppressed( $source, $error['type'], $error['lineno'] ) ) { + echo $error['orig'], "\n"; + $hasErrors = true; + } + } + } + + return $hasErrors; + } +} + +class CheckStyleSuppressor extends Suppressor { + /** + * @param string $input + * @return bool True do errors remain + */ + public function suppress( $input ) { + $dom = new DOMDocument(); + $dom->loadXML( $input ); + $hasErrors = false; + // DOMNodeList's are "live", convert to an array so it works as expected + $files = []; + foreach ( $dom->getElementsByTagName( 'file' ) as $file ) { + $files[] = $file; + } + foreach ( $files as $file ) { + $errors = []; + foreach ( $file->getElementsByTagName( 'error' ) as $error ) { + $errors[] = $error; + } + $source = file( $file->getAttribute( 'name' ) ); + $fileHasErrors = false; + foreach ( $errors as $error ) { + $lineno = $error->getAttribute( 'line' ) - 1; + $type = $error->getAttribute( 'source' ); + if ( $this->isSuppressed( $source, $type, $lineno ) ) { + $error->parentNode->removeChild( $error ); + } else { + $fileHasErrors = true; + $hasErrors = true; + } + } + if ( !$fileHasErrors ) { + $file->parentNode->removeChild( $file ); + } + } + echo $dom->saveXML(); + + return $hasErrors; + } +} + +class NoopSuppressor extends Suppressor { + private $mode; + + public function __construct( $mode ) { + $this->mode = $mode; + } + public function suppress( $input ) { + echo "Unsupported output mode: {$this->mode}\n$input"; + return true; + } +} + +$opt = getopt( "m:", [ "output-mode:" ] ); +// if provided multiple times getopt returns an array +if ( isset( $opt['m'] ) ) { + $mode = $opt['m']; +} elseif ( isset( $mode['output-mode'] ) ) { + $mode = $opt['output-mode']; +} else { + $mode = 'text'; +} +if ( is_array( $mode ) ) { + // If an option is passed multiple times getopt returns an + // array. Just take the last one. + $mode = end( $mode ); +} + +switch ( $mode ) { +case 'text': + $suppressor = new TextSuppressor(); + break; +case 'checkstyle': + $suppressor = new CheckStyleSuppressor(); + break; +default: + $suppressor = new NoopSuppressor( $mode ); +} + +$input = file_get_contents( 'php://stdin' ); +$hasErrors = $suppressor->suppress( $input ); + +if ( $hasErrors ) { + exit( 1 ); +} |