public function lint($files = [], $cache = true)
    {
        if (empty($files)) {
            $files = $this->getFiles();
        }

        $processCallback = is_callable($this->processCallback) ? $this->processCallback : function () {
        };

        $errors = [];
        $running = [];
        $newCache = [];

        while (!empty($files) || !empty($running)) {
            for ($i = count($running); !empty($files) && $i < $this->processLimit; ++$i) {
                $file = array_shift($files);
                $filename = $file->getRealPath();
                $relativePathname = $file->getRelativePathname();
                if (!isset($this->cache[$relativePathname]) || $this->cache[$relativePathname] !== md5_file($filename)) {
                    $lint = $this->createLintProcess($filename);
                    $running[$filename] = [
                        'process' => $lint,
                        'file' => $file,
                        'relativePath' => $relativePathname,
                    ];
                    $lint->start();
                } else {
                    $newCache[$relativePathname] = $this->cache[$relativePathname];
                }
            }

            foreach ($running as $filename => $item) {
                /** @var Lint $lint */
                $lint = $item['process'];
                if ($lint->isRunning()) {
                    continue;
                }

                unset($running[$filename]);
                if ($lint->hasSyntaxError()) {
                    $processCallback('error', $item['file']);
                    $errors[$filename] = array_merge(['file' => $filename], $lint->getSyntaxError());
                } else {
                    $newCache[$item['relativePath']] = md5_file($filename);
                    $processCallback('ok', $item['file']);
                }
            }
        }

        $cache && Cache::put($newCache);

        return $errors;
    }