/** * Generates a mutant file from a mutation * @param Mutation $mutation * * @return string */ public function generateFile(Mutation $mutation) { $id = $this->createId($mutation); // uniqid() $file = $this->tempDirectory . '/mutant.humbug.' . $id . '.php'; // generate mutated file $mutatorClass = $mutation->getMutator(); $originalFileContent = file_get_contents($mutation->getFile()); $tokens = Tokenizer::getTokens($originalFileContent); $mutatedFileContent = $mutatorClass::mutate($tokens, $mutation->getIndex()); file_put_contents($file, $mutatedFileContent); return $file; }
/** * Replace any return statement contained in brackets with null (but retain * the statement and move to before the return call). * This isn't perfect - the statement might evaluate to null anyway. * * @param array $tokens * @param int $index * @return array */ public static function getMutation(array &$tokens, $index) { $replace = []; $last = null; $tokenCount = count($tokens); for ($i = $index + 1; $i < $tokenCount; $i++) { if (is_array($tokens[$i]) && $tokens[$i][0] == T_WHITESPACE) { continue; } elseif (!is_array($tokens[$i]) && $tokens[$i] == '(') { // collect statement tokens (skipping one whitespace after 'return') for ($j = $index + 2; $j < $tokenCount; $j++) { if (!is_array($tokens[$j]) && $tokens[$j] == ';') { $last = $j - 1; break; } $replace[$j] = $tokens[$j]; } // replace them all with blanks and set last to 'null' foreach ($replace as $k => $t) { if ($k == $last) { $tokens[$k] = [T_STRING, 'null']; } else { $tokens[$k] = ''; } } // shift the instantiation prior to the return statement to // preserve instantiation behaviour without overwriting anything // and without upsetting line count. $replace[] = ';'; $replace[] = [T_WHITESPACE, ' ']; $string = ['-1' => Tokenizer::reconstructFromTokens($replace)]; array_splice($tokens, $index, 0, $string); break; } } }
/** * Based on the current file, generate mutations * * @return void */ public function generate() { $source = file_get_contents($this->getFilename()); $tokens = Tokenizer::getTokens($source); $lineNumber = 1; $methodName = '???'; $className = '???'; $namespace = ''; $inMethod = false; $inMethodBlock = false; $methodCurlyCount = 0; $tokenCount = count($tokens); foreach ($tokens as $index => $token) { if (is_array($token) && $token[0] == Tokenizer::T_NEWLINE) { $lineNumber = $token[2] + 1; continue; } if (is_array($token) && $token[0] == T_NAMESPACE) { for ($j = $index + 1; $j < $tokenCount; $j++) { if ($tokens[$j][0] == T_STRING) { $namespace .= '\\' . $tokens[$j][1]; } elseif ($tokens[$j] == '{' || $tokens[$j] == ';') { break; } } continue; } if (is_array($token) && ($token[0] == T_CLASS || $token[0] == T_INTERFACE) && $tokens[$index - 1][0] !== T_DOUBLE_COLON) { $className = $namespace . '\\' . $tokens[$index + 2][1]; continue; } //TODO: handle whitespace! if (is_array($token) && $token[0] == T_FUNCTION) { if (!isset($tokens[$index + 2][1])) { continue; // ignore closure } $methodName = $tokens[$index + 2][1]; $inMethod = true; continue; } /** * Limit mutation generation to the interior of methods (for now!) */ if (true === $inMethod && false === $inMethodBlock && $methodCurlyCount == 0 && !($token == '{' || is_array($token) && $token[0] == T_CURLY_OPEN)) { continue; //skip over argument list } if ($inMethod) { if ($token == '{' || is_array($token) && $token[0] == T_CURLY_OPEN) { $methodCurlyCount += 1; $inMethodBlock = true; continue; } elseif ($token == '}') { $methodCurlyCount -= 1; continue; } if ($methodCurlyCount == 0) { $inMethodBlock = false; $inMethod = false; continue; } } if (true === $inMethodBlock) { foreach ($this->mutators as $mutator) { if ($mutator::mutates($tokens, $index)) { $this->mutations[] = new Mutation($this->getFilename(), $lineNumber, $className, $methodName, $index, $mutator); } } } } return $this; }
/** * Perform a mutation against the given original source code tokens for * a mutable element * * @param array $tokens * @param int $index * @return string */ public static function mutate(array &$tokens, $index) { static::getMutation($tokens, $index); return Tokenizer::reconstructFromTokens($tokens); }