Пример #1
0
 public function getResultIterator($path)
 {
     if ((Environment::isWindows() || Environment::isLinuxWithWine()) !== true) {
         $this->log(['FxCop requires Windows or Wine to execute', \Yasca\Logs\Level::ERROR]);
         return new \EmptyIterator();
     }
     $compatibleVersions = ['Microsoft Fxcop 10.0', 'Microsoft Fxcop 1.36', 'Microsoft Fxcop 1.35'];
     $pluginDir = (new \Yasca\Core\IteratorBuilder())->from([__DIR__])->concat((new \Yasca\Core\IteratorBuilder())->from($compatibleVersions)->select(static function ($versionDir) {
         return __DIR__ . '/' . $versionDir;
     }))->concat((new \Yasca\Core\IteratorBuilder())->from(['ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432'])->where(static function ($specialDir) {
         return isset($_ENV[$specialDir]);
     })->select(static function ($specialDir) {
         return $_ENV[$specialDir];
     })->selectMany(static function ($dir) use($compatibleVersions) {
         return (new \Yasca\Core\IteratorBuilder())->from($compatibleVersions)->select(static function ($versionDir) use($dir) {
             return "{$dir}/{$versionDir}";
         });
     }))->where(static function ($dir) {
         return (new \SplFileInfo("{$dir}/FxCopCmd.exe"))->isFile();
     })->firstOrNull();
     if ($pluginDir === null) {
         $this->log(['FxCop cannot be found. To enable the FxCop plugin, ' . 'install it from Microsoft in the default location ' . ' or copy FxCop to the ./Yasca/Plugins/FxCop directory.', \Yasca\Logs\Level::ERROR]);
         return new \EmptyIterator();
     }
     try {
         $process = new Process((Environment::isLinuxWithWine() === true ? 'wine ' : '') . '"' . $pluginDir . '/FxCopCmd.exe" ' . '/ignoreinvalidtargets ' . '/searchgac ' . '/rule:"' . $pluginDir . '/Rules/SecurityRules.dll" ' . '/consolexsl:"' . __DIR__ . '/yasca.xsl" ' . '/quiet ' . (new \Yasca\Core\IteratorBuilder())->from(new \RecursiveDirectoryIterator($path))->whereRegex('`(?i)\\.(' . (new \Yasca\Core\IteratorBuilder())->from($this->getSupportedFileTypes())->select(static function ($element) {
             return \preg_quote($element, '`');
         })->join('|') . ')$`u')->select(static function ($current) {
             return " /file:\"{$current}\"";
         })->join(''));
     } catch (ProcessStartException $e) {
         $this->log(['FxCop failed to start', \Yasca\Logs\Level::ERROR]);
         return new \EmptyIterator();
     }
     $this->log(['FxCop launched', \Yasca\Logs\Level::INFO]);
     return $process->continueWith(function ($async) {
         list($stdout, $stderr) = $async->result();
         $this->log(['FxCop completed', \Yasca\Logs\Level::INFO]);
         $dom = new \DOMDocument();
         try {
             $success = $dom->loadXML($stdout);
         } catch (\ErrorException $e) {
             $success = false;
         }
         if ($success !== true) {
             if ($stdout === '') {
                 $this->log(['FxCop did not return any data', \Yasca\Logs\Level::ERROR]);
             } else {
                 $this->log(['FxCop did not return valid XML', \Yasca\Logs\Level::ERROR]);
                 $this->log(["FxCop returned {$stdout}", \Yasca\Logs\Level::ERROR]);
             }
             return Async::fromResult(new \EmptyIterator());
         }
         return (new \Yasca\Core\IteratorBuilder())->from($dom->getElementsByTagName('result'))->select(static function ($result) {
             return (new \Yasca\Result())->setOptions(['pluginName' => 'FxCop', 'severity' => "{$result->getAttribute('severity')}", 'category' => "{$result->getAttribute('category')}", 'filename' => "{$result->getAttribute('filename')}", 'references' => ["{$result->getAttribute('reference')}" => 'MSDN'], 'message' => "{$result->getAttribute('message')}", 'description' => "{$result->getAttribute('description')}"]);
         })->toFunctionPipe()->pipe([Async::_class, 'fromResult']);
     });
 }
Пример #2
0
    public function getResultIterator($path)
    {
        if (Environment::hasAtLeastJavaVersion(5) !== true) {
            $this->log(['FindBugs requires JRE 1.5 or later.', \Yasca\Logs\Level::ERROR]);
            return new \EmptyIterator();
        }
        try {
            $process = new Process('"' . __DIR__ . '/bin/findbugs' . (Environment::isWindows() ? '.bat' : '') . '"' . ' -home "' . __DIR__ . '" ' . ' -include "' . __DIR__ . '/filter.xml" ' . '-textui -xml:withMessages -xargs -quiet');
        } catch (ProcessStartException $e) {
            $this->log(['FindBugs failed to start', \Yasca\Logs\Level::ERROR]);
            return new \EmptyIterator();
        }
        $this->log(['FindBugs launched', \Yasca\Logs\Level::INFO]);
        (new \Yasca\Core\FunctionPipe())->wrap($path)->pipe([Operators::_class, '_new'], '\\RecursiveDirectoryIterator')->toIteratorBuilder()->where(function ($fileinfo) {
            return $this->supportsExtension($fileinfo->getExtension());
        })->select(static function ($fileinfo, $filepath) {
            return "{$filepath}\n";
        })->forAll([$process, 'writeToStdin']);
        $process->closeStdin();
        return $process->continueWith(function ($async) use($path) {
            list($stdout, $stderr) = $async->result();
            $this->log(['FindBugs completed', \Yasca\Logs\Level::INFO]);
            $dom = new \DOMDocument();
            try {
                $success = $dom->loadXML($stdout);
            } catch (\ErrorException $e) {
                $success = false;
            }
            if ($success !== true) {
                if ($stdout === '') {
                    $this->log(['FindBugs did not return any data', \Yasca\Logs\Level::ERROR]);
                } else {
                    $this->log(['FindBugs did not return valid XML', \Yasca\Logs\Level::ERROR]);
                    $this->log(["FindBugs returned {$stdout}", \Yasca\Logs\Level::ERROR]);
                }
                return Async::fromResult(new \EmptyIterator());
            }
            $bugPatterns = (new \Yasca\Core\IteratorBuilder())->from($dom->getElementsByTagName('BugPattern'))->selectKeys(static function ($patternNode) {
                return ["{$pattern->getElementsByTagName('Details')->item(0)->nodeValue}", "{$pattern->getAttribute('type')}"];
            })->toArray(true);
            return (new \Yasca\Core\IteratorBuilder())->from($dom->getElementsByTagName('BugInstance'))->select(static function ($bugInstance) use(&$bugPatterns, $path) {
                $type = $bugInstance->getAttribute('type');
                $sourceLine = $bugInstance->getElementsByTagName('SourceLine')->item(0);
                $shortMessage = $bugInstance->getElementsByTagName('ShortMessage')->item(0)->nodeValue;
                return (new \Yasca\Result())->setOptions(['pluginName' => 'FindBugs', 'severity' => "{$bugInstance->getAttribute('priority')}", 'category' => (new \Yasca\Core\FunctionPipe())->wrap($bugInstance->getAttribute('category'))->pipeLast('\\str_replace', '_', ' ')->pipe('\\strtolower')->pipe('\\ucwords')->unwrap(), 'lineNumber' => "{$sourceLine->getAttribute('start')}", 'filename' => "{$path}/{$sourceLine->getAttribute('sourcepath')}", 'references' => ['http://findbugs.sourceforge.net/bugDescriptions.html#' . \urlencode($type) => 'FindBugs Bug Description'], 'message' => "{$shortMessage}", 'description' => <<<EOT
{$shortMessage}

{$bugPatterns[$type]}
EOT
]);
            })->toFunctionPipe()->pipe([Async::_class, 'fromResult']);
        });
    }
Пример #3
0
 public function getResultIterator($path)
 {
     if (Environment::hasAtLeastJavaVersion(4) !== true) {
         $this->log(['PMD requires JRE 1.4 or later.', \Yasca\Logs\Level::ERROR]);
         return new \EmptyIterator();
     }
     try {
         $process = new Process('java -cp "' . (new \Yasca\Core\FunctionPipe())->wrap(__DIR__)->pipe([Operators::_class, '_new'], '\\FilesystemIterator')->toIteratorBuilder()->select(static function ($u, $key) {
             return $key;
         })->whereRegex('`\\.jar$`ui')->join(PATH_SEPARATOR) . '" net.sourceforge.pmd.PMD "' . $path . '"' . ' xml' . ' "' . __DIR__ . '/yasca-rules.xml"');
         //    ' "' . __DIR__ . '/yasca-rules.xml"'
         //);
     } catch (ProcessStartException $e) {
         $this->log(['PMD failed to start', \Yasca\Logs\Level::ERROR]);
         return new \EmptyIterator();
     }
     $this->log(['PMD launched', \Yasca\Logs\Level::INFO]);
     return $process->continueWith(function ($async) {
         list($stdout, $stderr) = $async->result();
         $this->log(['PMD completed', \Yasca\Logs\Level::INFO]);
         //$this->log([$stdout, \Yasca\Logs\Level::ERROR]);
         $dom = new \DOMDocument();
         try {
             $success = $dom->loadXML($stdout);
         } catch (\ErrorException $e) {
             $success = false;
         }
         if ($success !== true) {
             if ($stdout === '') {
                 $this->log(['PMD did not return any data', \Yasca\Logs\Level::ERROR]);
                 $this->log([$stderr, \Yasca\Logs\Level::ERROR]);
             } else {
                 $this->log(['PMD did not return valid XML', \Yasca\Logs\Level::ERROR]);
                 $this->log(["PMD returned {$stdout}", \Yasca\Logs\Level::ERROR]);
             }
             return Async::fromResult(new \EmptyIterator());
         }
         return (new \Yasca\Core\IteratorBuilder())->from($dom->getElementsByTagName('file'))->selectMany(static function ($fileNode) {
             return (new \Yasca\Core\IteratorBuilder())->from($fileNode->getElementsByTagName('violation'))->select(static function ($violationNode) use($fileNode) {
                 return (new \Yasca\Result())->setOptions(['pluginName' => 'PMD', 'filename' => "{$fileNode->getAttribute('name')}", 'lineNumber' => "{$violationNode->getAttribute('beginline')}", 'category' => "{$violationNode->getAttribute('rule')}", 'severity' => "{$violationNode->getAttribute('priority')}", 'description' => "", 'message' => "", 'references' => ["{$violationNode->getAttribute('externalInfoUrl')}" => 'PMD Reference']]);
             });
         })->toFunctionPipe()->pipe([Async::_class, 'fromResult']);
     });
 }
Пример #4
0
    public function getResultIterator($path)
    {
        if (Environment::isWindows() !== true) {
            $this->log(['The copy of CppCheck included with Yasca requires Windows', \Yasca\Logs\Level::ERROR]);
            return new \EmptyIterator();
        }
        try {
            $process = new Process('"' . __DIR__ . '/cppcheck" ' . '--quiet ' . '--enable=all ' . '--inline-suppr ' . '--xml ' . '"' . $path . '"');
        } catch (ProcessStartException $e) {
            $this->log(['CppCheck failed to start', \Yasca\Logs\Level::ERROR]);
            return new \EmptyIterator();
        }
        $this->log(['CppCheck launched', \Yasca\Logs\Level::INFO]);
        return $process->continueWith(function ($async) {
            list($stdout, $stderr) = $async->result();
            $this->log(['CppCheck completed', \Yasca\Logs\Level::INFO]);
            $regex = <<<'EOT'
`No C or C\+\+ source files found\.`u
EOT;
            if (\preg_match($regex, $stderr)) {
                $this->log(['CppCheck did not find any C or C++ source files', \Yasca\Logs\Level::ERROR]);
                return Async::fromResult(new \EmptyIterator());
            }
            $dom = new \DOMDocument();
            try {
                $success = $dom->loadXML($stderr);
            } catch (\ErrorException $e) {
                $success = false;
            }
            if ($success !== true) {
                if ($stderr === '') {
                    $this->log(['CppCheck did not return any data', \Yasca\Logs\Level::ERROR]);
                } else {
                    $this->log(['CppCheck did not return valid XML', \Yasca\Logs\Level::ERROR]);
                    $this->log(["CppCheck returned {$stderr}", \Yasca\Logs\Level::ERROR]);
                }
                return Async::fromResult(new \EmptyIterator());
            }
            return (new \Yasca\Core\IteratorBuilder())->from($dom->getElementsByTagName('error'))->select(static function ($errorNode) {
                return (new \Yasca\Result())->setOptions(['pluginName' => 'CppCheck', 'category' => "{$errorNode->getAttribute('id')}", 'lineNumber' => "{$errorNode->getAttribute('line')}", 'filename' => "{$errorNode->getAttribute('file')}", 'message' => "{$errorNode->getAttribute('msg')}", 'description' => "{$errorNode->getAttribute('msg')}", 'references' => ['http://sourceforge.net/projects/cppcheck/' => 'CppCheck Home Page'], 'severity' => (new \Yasca\Core\FunctionPipe())->wrap($errorNode->getAttribute('severity'))->pipe(static function ($cppcheckSeverity) {
                    //http://cppcheck.sourceforge.net/devinfo/doxyoutput/classSeverity.html
                    if ($cppcheckSeverity === 'error') {
                        return 2;
                    } elseif ($cppcheckSeverity === 'warning') {
                        return 3;
                    } elseif ($cppcheckSeverity === 'portability' || $cppcheckSeverity === 'performance' || $cppcheckSeverity === 'style' || $cppcheckSeverity === 'portability') {
                        return 4;
                    } else {
                        //$cppcheckSeverity === 'debug'
                        //$cppcheckSeverity === 'information'
                        //$cppcheckSeverity === 'none'
                        return 5;
                    }
                })->unwrap()]);
            })->where(static function ($result) {
                $category = $result->category;
                if ($category === 'toomanyconfigs' || $category === 'syntaxError' || $category === 'cppcheckError') {
                    return false;
                } else {
                    return true;
                }
            })->toFunctionPipe()->pipe([Async::_class, 'fromResult']);
        });
    }
Пример #5
0
	public function __construct($options){
		list($subscribeIfCloseable, $closeSubscribedCloseables) =
			Operators::invoke(static function(){
				$closeables = new \SplObjectStorage();
				return [
					static function($object) use ($closeables){
						if (
							(new \Yasca\Core\IteratorBuilder)
							->from(Iterators::traitsOf($object))
							->contains('Yasca\Core\Closeable')
						){
							$closeables->attach($object);
						}
					},
					static function() use ($closeables){
						foreach($closeables as $closeable){
							$closeable->close();
						}
						$closeables->removeAllExcept(new \SplObjectStorage());
					},
				];
			});

		list($fireLogEvent, $fireResultEvent) =
			Operators::invoke(function() use ($subscribeIfCloseable){
				$newEvent = function($name) use ($subscribeIfCloseable){
					$event = new SplSubjectAdapter();
					$this->{"attach{$name}Observer"} = function(\SplObserver $observer) use ($event, $subscribeIfCloseable){
						$event->attach($observer);
						$subscribeIfCloseable($observer);
						return $this;
					};
					$this->{"detach{$name}Observer"} = function(\SplObserver $observer) use ($event, $subscribeIfCloseable){
						$event->detach($observer);
						$subscribeIfCloseable($observer);
						return $this;
					};
					return static function($value) use ($event){
						$event->raise($value);
					};
				};
				return [
					$newEvent('Log'),
					$newEvent('Result'),
				];
			});

		$targetDirectory =
			(new \Yasca\Core\FunctionPipe)
			->wrap($options)
			->pipe([Iterators::_class,'elementAtOrNull'], 'targetDirectory')
			->pipe('\realpath')
			->unwrap();

		$makeRelative =
			//Make filenames relative when publishing a result
			(new \Yasca\Core\FunctionPipe)
			->wrap($targetDirectory)
			->pipe('\preg_quote', '`')
			->pipe(static function($dirLiteral) { return "`^$dirLiteral`ui"; })
			->pipe(static function($regex){
				return Operators::curry('\preg_replace', $regex, '');
			})
			->unwrap();

		//Wrap Result event trigger to make changes to each Result
		$fireResultEvent = static function(Result $result) use ($fireResultEvent, $makeRelative){
			//Make adjustments based on adjustments data
			(new \Yasca\Core\FunctionPipe)
			->wrap(static::$adjustments)
			->pipe([Iterators::_class, 'elementAtOrNull'], $result->pluginName)
			->pipe([Iterators::_class, 'elementAtOrNull'], $result->category)
			->pipe(static function($options) use ($result){
				if ($options !== null){
					$result->setOptions($options);
				}
			});

			//Get unsafeSourceCode if needed, and then make the filename relative
			//to the scan directory
			if (isset($result->filename) === true && !Operators::isNullOrEmpty($result->filename)){
				if(isset($result->lineNumber) === true && isset($result->unsafeSourceCode) !== true){
					try {
						$result->unsafeSourceCode =
							(new \Yasca\Core\FunctionPipe)
							->wrap($result->filename)
							->pipe([Encoding::_class,'getFileContentsAsArray'])
							->toIteratorBuilder()
							->slice(
								\max($result->lineNumber - 10, 0),
								20
							)
							->toArray(true);
					} catch (\ErrorException $e){
						$tail = 'No such file or directory';
						if (\substr($e->getMessage(),0-strlen($tail)) === $tail){
							//External tool generated a filename that's not present
							//FindBugs can often do this if the matching .java files are missing.
						} else {
							throw $e;
						}
					}
				}
				$result->setOptions([
					'filename' => "{$makeRelative($result->filename)}",
				]);
			}
			$fireResultEvent($result);
		};

		$createPlugins =
			Operators::curry(
				static function($ignoreRegex, $onlyRegex) use ($fireLogEvent){
					$retval =
						(new \Yasca\Core\IteratorBuilder)
						->from(Plugin::$installedPlugins)
						->select(static function($plugins) use ($ignoreRegex, $onlyRegex, $fireLogEvent){
							return (new \Yasca\Core\IteratorBuilder)
							->from($plugins)
							->whereRegex($ignoreRegex)
							->whereRegex($onlyRegex)
							->select(static function($pluginName) use ($fireLogEvent){
								$p = new $pluginName($fireLogEvent);
								$fireLogEvent(["Plugin $pluginName Loaded", \Yasca\Logs\Level::DEBUG]);
								return $p;
							})
							->toObjectStorage();
						})
						->where([Iterators::_class,'any'])
						->toArray(true);
					$fireLogEvent(['Selected Plugins Loaded', \Yasca\Logs\Level::DEBUG]);
					return $retval;
				},
				(new \Yasca\Core\FunctionPipe)
				->wrap($options)
				->pipe([Iterators::_class, 'elementAtOrNull'], 'pluginsIgnore')
				->toIteratorBuilder()
				->select(static function($literal){return \preg_quote($literal, '`');})
				->toFunctionPipe()
				->pipe([Iterators::_class, 'join'], '|')
				->pipe(static function($string){
					if (Operators::isNullOrEmpty($string) === true){
						return null;
					} else {
						return "`^(?!.*($string).*$)`u";
					}
				})
				->unwrap(),
				(new \Yasca\Core\FunctionPipe)
				->wrap($options)
				->pipe([Iterators::_class, 'elementAtOrNull'], 'pluginsOnly')
				->toIteratorBuilder()
				->select(static function($literal){return \preg_quote($literal, '`');})
				->toFunctionPipe()
				->pipe([Iterators::_class, 'join'], '|')
				->pipe(static function($string){
					if (Operators::isNullOrEmpty($string) === true){
						return null;
					} else {
						return "`($string)`u";
					}
				})
				->unwrap()
			);

		$createTargetIterator =
			Operators::curry(
				static function($extensionRegex, $extensionsIgnoreRegex, $extensionsOnlyRegex, $pluginArray) use ($targetDirectory){
					//Only select files that plugins ask for
					return (new \Yasca\Core\IteratorBuilder)
					->from(new \RecursiveDirectoryIterator(
						$targetDirectory,
						\FilesystemIterator::KEY_AS_PATHNAME 	 |
						\FilesystemIterator::CURRENT_AS_FILEINFO |
						\FilesystemIterator::UNIX_PATHS
					))
					->whereRegex($extensionRegex($pluginArray), \RegexIterator::MATCH, \RegexIterator::USE_KEY)
					->whereRegex($extensionsIgnoreRegex, \RegexIterator::MATCH, \RegexIterator::USE_KEY)
					->whereRegex($extensionsOnlyRegex, \RegexIterator::MATCH, \RegexIterator::USE_KEY)
					;
				},
				static function($pluginArray){
					return (new \Yasca\Core\IteratorBuilder)
					->from($pluginArray)
					->selectMany(static function($plugins){
						return (new \Yasca\Core\IteratorBuilder)
						->from($plugins);
					})
					->selectMany(static function($plugin){
						return (new \Yasca\Core\IteratorBuilder)
						->from($plugin->getSupportedFileTypes());
					})
					->unique()
					->select(static function($ext){
						return \preg_quote($ext, '`');
					})
					->toFunctionPipe()
					->pipe([Iterators::_class, 'join'], '|')
					->pipe(static function($string){
						if (Operators::isNullOrEmpty($string) === true){
							return null;
						} else {
							return "`\.($string)$`ui";
						}
					})
					->unwrap();
				},
				(new \Yasca\Core\FunctionPipe)
				->wrap($options)
				->pipe([Iterators::_class, 'elementAtOrNull'], 'extensionsIgnore')
				->toIteratorBuilder()
				->select(static function($ext){return '.' . \trim($ext, '.');})
				->select(static function($literal){return \preg_quote($literal, '`');})
				->toFunctionPipe()
				->pipe([Iterators::_class, 'join'], '|')
				->pipe(static function($string){
					if (Operators::isNullOrEmpty($string) === true){
						return null;
					} else {
						return "`(?<!$string)$`ui";
					}
				})
				->unwrap(),
				(new \Yasca\Core\FunctionPipe)
				->wrap($options)
				->pipe([Iterators::_class, 'elementAtOrNull'], 'extensionsOnly')
				->toIteratorBuilder()
				->select(static function($ext){return '.' . \trim($ext, '.');})
				->select(static function($literal){return \preg_quote($literal, '`');})
				->toFunctionPipe()
				->pipe([Iterators::_class, 'join'], '|')
				->pipe(static function($string){
					if (Operators::isNullOrEmpty($string) === true){
						return null;
					} else {
						return "`($string)$`ui";
					}
				})
				->unwrap()
			);

		$debug =
			(new \Yasca\Core\FunctionPipe)
			->wrap($options)
			->pipe([Iterators::_class,'elementAtOrNull'], 'debug')
			->pipe([Operators::_class,'equals'], true)
			->unwrap();

		$processResults =
			static function($results) use ($fireResultEvent, &$processResults){
				if ($results instanceof Result){
					$fireResultEvent($results);
					return new \EmptyIterator();
				} elseif ($results instanceof Async){
					if ($results->isDone() === true){
						return $processResults($results->result());
					} else {
						return Iterators::ensureIsIterator([$results]);
					}
				} elseif ($results instanceof Wrapper){
					return $processResults($results->unwrap());
				} else {
					return (new \Yasca\Core\IteratorBuilder)
					->from($results)
					->selectMany($processResults);
				}
			};

		$this->executeAsync = static function() use (
			$fireLogEvent,
			$processResults,
			$closeSubscribedCloseables, $debug,
			$makeRelative, $createPlugins,
			$targetDirectory, $createTargetIterator
		){
			try {
				$fireLogEvent(['Yasca ' . Scanner::VERSION . ' - http://www.yasca.org/ - Michael V. Scovetta', \Yasca\Logs\Level::INFO]);
				$fireLogEvent(["Scanning $targetDirectory", \Yasca\Logs\Level::INFO]);


				$plugins = $createPlugins();

				$multicasts = Iterators::elementAtOrNull($plugins, __NAMESPACE__ . '\MulticastPlugin');

				$lastStatusReportedTime = \time();
				$filesProcessed = 0;
				$awaits = [];
				foreach($createTargetIterator($plugins) as $filePath => $targetFileInfo){
					$fireLogEvent(["Checking file {$makeRelative($filePath)}", \Yasca\Logs\Level::DEBUG]);

					$n = \time();
					if ($n - $lastStatusReportedTime > self::SECONDS_PER_NOTIFY){
						$fireLogEvent(["$filesProcessed files scanned", \Yasca\Logs\Level::INFO]);
						$lastStatusReportedTime = $n;
					}

					$ext = $targetFileInfo->getExtension();
					$getFileContents =
						(new \Yasca\Core\FunctionPipe)
						->wrap($filePath)
						->pipeLast([Operators::_class,'curry'], [Encoding::_class, 'getFileContentsAsArray'])
						->pipe([Operators::_class,'lazy'])
						->unwrap();

					$awaits =
						(new \Yasca\Core\IteratorBuilder)
						->from($awaits)
						->concat(
							//Multicast plugin results
							(new \Yasca\Core\IteratorBuilder)
							->from($multicasts)

							//Make a copy to allow removing elements iterated over
							->toFunctionPipe()
							->pipe([Iterators::_class,'toList'])
							->toIteratorBuilder()

							->where(static function($plugin) use ($ext){
								return $plugin->supportsExtension($ext);
							})
							->select(static function($plugin) use ($multicasts, $targetDirectory){
								$multicasts->detach($plugin);
								return $plugin->getResultIterator($targetDirectory);
							}),

							//Single file path plugin results
							(new \Yasca\Core\FunctionPipe)
							->wrap($plugins)
							->pipe([Iterators::_class, 'elementAtOrNull'], __NAMESPACE__ . '\SingleFilePathPlugin')
							->toIteratorBuilder()
							->where(static function($plugin) use ($ext){
								return $plugin->supportsExtension($ext);
							})
							->select(static function($plugin) use ($filePath){
								return $plugin->getResultIterator($filePath);
							}),

							//Single file contents plugin results
							(new \Yasca\Core\FunctionPipe)
							->wrap($plugins)
							->pipe([Iterators::_class, 'elementAtOrNull'], __NAMESPACE__ . '\SingleFileContentsPlugin')
							->toIteratorBuilder()
							->where(static function($plugin) use ($ext){
								return $plugin->supportsExtension($ext);
							})
							->select(static function($plugin) use ($getFileContents, $filePath){
								return $plugin->getResultIterator($getFileContents(), $filePath);
							})
						)
						->selectMany($processResults)
						->toList();

					(new \Yasca\Core\FunctionPipe)
					->wrap($plugins)
					->pipe([Iterators::_class, 'elementAtOrNull'], __NAMESPACE__ . '\AggregateFileContentsPlugin')
					->toIteratorBuilder()
					->where(static function($plugin) use ($ext){
						return $plugin->supportsExtension($ext);
					})
					->forAll(static function($plugin) use ($getFileContents, $filePath){
						$plugin->apply($getFileContents(), $filePath);
					});

					Async::tickAll();
					$filesProcessed += 1;
				}
				$fireLogEvent(['Finished with files. Gathering results from Aggregate plugins', \Yasca\Logs\Level::DEBUG]);

				$awaits =
					(new \Yasca\Core\FunctionPipe)
					->wrap($plugins)
					->pipe([Iterators::_class, 'elementAtOrNull'], __NAMESPACE__ . '\AggregateFileContentsPlugin')
					->toIteratorBuilder()
					->select(static function($plugin){return $plugin->getResultIterator();})
					->concat($awaits)
					->selectMany($processResults)
					->toList();
				if (Iterators::any($awaits) === true){
					$fireLogEvent(['Waiting on external plugins', \Yasca\Logs\Level::INFO]);
					return (new Async(
						static function() use (
							&$awaits, $processResults, $fireLogEvent
						){
							$awaits =
								(new \Yasca\Core\IteratorBuilder)
								->from($awaits)
								->selectMany($processResults)
								->toList();
							return Iterators::any($awaits) === false;
						},
						static function() use ($fireLogEvent) {
							$fireLogEvent(['Scan complete', \Yasca\Logs\Level::INFO]);
							return null;
						},
						static function(\Exception $exception) use ($fireLogEvent, $debug){
							$fireLogEvent(['Scan aborted', \Yasca\Logs\Level::ERROR]);
							if ($debug === true){
								throw $exception;
							} else {
								$fireLogEvent([$exception->getMessage(), \Yasca\Logs\Level::ERROR]);
								return null;
							}
						}
					))
					->whenDone($closeSubscribedCloseables);
				} else {
					$fireLogEvent(['Scan complete', \Yasca\Logs\Level::INFO]);
					return Async::fromResult(null)->whenDone($closeSubscribedCloseables);
				}
			} catch (\Exception $exception){
				$fireLogEvent(['Scan aborted', \Yasca\Logs\Level::ERROR]);
				if ($debug === true) {
					$closeSubscribedCloseables();
					throw $exception;
				} else {
					$fireLogEvent([$exception->getMessage(), \Yasca\Logs\Level::ERROR]);
					return Async::fromResult(null)->whenDone($closeSubscribedCloseables);
				}
			}
		};

		$this->execute = function(){
			$f = $this->executeAsync;
			return $f()->result();
		};
	}