01.062012

In Eclipse PDT nicht initialisierte Variablen finden

Wer von PHPEclipse zu PDT gewechselt hat, dürfte, gerade in größeren Projekten, schnell ein Feature vermissen. Mit PHPEclipse werden nicht initialisierte Variablen markiert, mit PDT nicht. So können z.B. ärgerliche Tippfehler nicht sofort erkannt und behoben werden.  Wer das schmerzlich vermisst kann mit einem DLTK Validator für Abhilfe sorgen.

Ein Validator Script muss erst einmal nichts weiter machen, als einen Dateienamen entgegen zu nehmen, und Fehler oder Warnungen mit Dateiname, Zeile und Fehlertext auszugeben. Für das Format der Ausgabe kann man dann in Eclipse ein entsprechende Regel anlegen, so das die Fehler entsprechend im Editor markiert werden.

Mittels der folgenden PHP Scriptes werden exemplarisch die Methoden der Klassen einer Datei untersucht und nicht initialisierte Variablen als Fehler gemeldet.

#!/usr/bin/php
<php
/**
  * Detect the classes in the given file.
  *
  * @param string $filename
  * @return array
  */
function getClassNames($filename) {
  $content = file_get_contents($filename);
  $classes = array();

  if(preg_match_all("~class ([a-zA-Z0-9_]+) ~", $content,
                    $matches)) {
    foreach($matches[1] as $className) {
      $classes[] = $className;
    }
  }
  return $classes;
}

/**
  * @param ReflectionMethod $method
  * @return string
  */
function getMethodSource( ReflectionMethod $method ) {
  $path = $method->getFileName();
  $lines = @file( $path );
  $from = $method->getStartLine();
  $to   = $method->getEndLine();
  $len  = $to-$from+1;
  return implode( array_slice( $lines, $from-1, $len ));
}

/**
  * @param ReflectionMethod $method
  *
  * @return array
  */
function checkUndeclaredVariables(ReflectionMethod $method) {
  $declaredVariables = array();
  $usedVariables = array();
  $undeclaredVariables = array();

  //Method parameters are declared parameters.
  $parameters = $method->getParameters();
  if(count($parameters) > 0) {
    foreach($parameters as $parameter) {
      $declaredVariables[] = $parameter->getName();
    }
  }

  $methodSource = getMethodSource($method);
  //Detect initilized variables.
  if(preg_match_all('~[^:]$([a-zA-Z0-9_]+)[s]*=[s]*~',
                    $methodSource, $matches)) {
    foreach($matches[1] as $variable) {
      $declaredVariables[] = $variable;
    }
  }
  //Variables in loops are initialized too. (TODO pattern does not match as $index => $value)
  if(preg_match_all('~as $([a-zA-Z0-9_]+)[s]*)~', $methodSource,
                    $matches)) {
    foreach($matches[1] as $variable) {
      $declaredVariables[] = $variable;
    }
  }
  // Finde all used variables.
  if(preg_match_all('~[^:]$([a-zA-Z0-9_]+)~m', $methodSource,
                    $matches)) {
    foreach($matches[1] as $index => $variable) {
      //Except those because they are mostly implicit.
      if(array_search($variable,array('this',
                                      'GLOBALS',
                                      '_SERVER',
                                      '_REQUEST',
                                      'exception')) === false) {
        $usedVariables[] = $variable;
      }
    }
  }

  $usedVariables = array_unique($usedVariables);
  $methodLines = explode("n", $methodSource);
  //Check if the used variables are declared and find them in the source if not.
  foreach($usedVariables as $variable) {
    if(array_search($variable, $declaredVariables) === false) {
      $onLines = preg_grep("~~", $methodLines);

      foreach($onLines as $lineNr => $line) {
        if(preg_match("~{$variable}~", $line)) {
          $undeclaredVariables[] = array(
              'variable' => $variable,
              'line' => ($method->getStartLine() + $lineNr),
          );
        }
      }
    }
  }

  return $undeclaredVariables;
}

/**
  * @param ReflectionClass $reflection_class
  * @param array &$errors
  * @return array
  */
function checkClass(ReflectionClass $reflection_class, &$errors) {
  if(!is_array($errors)) {
    $errors = array();
  }

  $fileName = basename($reflection_class->getFileName());
  $methods = $reflection_class->getMethods();
  foreach ($methods as $method) {
    $undeclaredVariales = checkUndeclaredVariables($method);
    if(!empty($undeclaredVariales)) {
      foreach($undeclaredVariales as $data) {
        $msg = "Undefined Variable '${$data['variable']}'"       
        $errors[] = "Error:{$fileName}:{$data['line']}:{$msg}";
      }
    }
  }
}

if($argc > 1) {
  $phpFile = $argv[1];

  if(file_exists($phpFile)) {
    $classes = getClassNames($phpFile);
    if(!empty($classes)) {
      $errors = array();

      require_once "${phpFile}";
      foreach ($classes as $className) {
        $reflectionClass = new ReflectionClass($className);
        checkClass($reflectionClass, $errors);
      }

      if(!empty($errors)) {
        print(implode("n", $errors));
      }
    }
  }
}

Eine genaue Anleitung, wie das Script als externer Validator eingebunden werden kann, findet sich hier. In Filename extensions muß 'php' und unter den Pattern Rules 'Error:%f:%n:%m' eingetragen werden. Dabei liegt die Frage was als Fehler oder Warnung gemeldet werden soll ganz im eigenen Ermessen.

Kleiner Tipp: das Script sollte nicht innerhalb eine Projektes hinterlegt sein, da Eclipse sonst, bei dem Versuch es mit sich selbst zu validieren, hängen bleibt.