diff --git a/bin/phplint b/bin/phplint
index b97cc9c8..5d33baa8 100755
--- a/bin/phplint
+++ b/bin/phplint
@@ -45,10 +45,17 @@ if (true === $input->hasParameterOption(['--no-progress'], true)) {
}
}
-$extensions[] = new OutputFormat([
+$formats = [
OptionDefinition::LOG_JSON,
OptionDefinition::LOG_JUNIT,
-]);
+];
+
+if (\class_exists('\Bartlett\Sarif\SarifLog')) {
+ // only when Composer development dependencies were installed too!
+ $formats[] = OptionDefinition::LOG_SARIF;
+}
+
+$extensions[] = new OutputFormat($formats);
$dispatcher = new EventDispatcher($extensions);
diff --git a/composer.json b/composer.json
index a005c299..29db4e7a 100644
--- a/composer.json
+++ b/composer.json
@@ -31,10 +31,11 @@
"symfony/yaml": "^5.4 || ^6.0"
},
"require-dev": {
- "php-parallel-lint/php-console-highlighter": "^1.0",
+ "bamarni/composer-bin-plugin": "^1.4",
+ "bartlett/sarif-php-sdk": "^1.0",
"brainmaestro/composer-git-hooks": "^2.8.5 || 3.0.0-alpha.1",
"jetbrains/phpstorm-stubs": "^2021.3 || ^2022.3",
- "bamarni/composer-bin-plugin": "^1.4"
+ "php-parallel-lint/php-console-highlighter": "^1.0"
},
"autoload": {
"psr-4": {
diff --git a/src/Command/ConfigureCommandTrait.php b/src/Command/ConfigureCommandTrait.php
index 3de75a39..7d0fb3cc 100644
--- a/src/Command/ConfigureCommandTrait.php
+++ b/src/Command/ConfigureCommandTrait.php
@@ -99,6 +99,12 @@ protected function configureCommand(Command $command): void
InputOption::VALUE_OPTIONAL,
'Log scan results in JUnit XML format to file (default: ' . OptionDefinition::DEFAULT_STANDARD_OUTPUT_LABEL . ')'
)
+ ->addOption(
+ 'log-sarif',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Log scan results in SARIF format to file (default: ' . OptionDefinition::DEFAULT_STANDARD_OUTPUT_LABEL . ')'
+ )
->addOption(
'warning',
'w',
diff --git a/src/Configuration/AbstractOptionsResolver.php b/src/Configuration/AbstractOptionsResolver.php
index 59f0021d..402aeb73 100644
--- a/src/Configuration/AbstractOptionsResolver.php
+++ b/src/Configuration/AbstractOptionsResolver.php
@@ -47,6 +47,7 @@ public function __construct(InputInterface $input, array $configuration = [])
OptionDefinition::NO_PROGRESS => false,
OptionDefinition::LOG_JSON => null,
OptionDefinition::LOG_JUNIT => null,
+ OptionDefinition::LOG_SARIF => false,
OptionDefinition::WARNING => false,
OptionDefinition::OPTION_MEMORY_LIMIT => ini_get('memory_limit'),
OptionDefinition::IGNORE_EXIT_CODE => false,
@@ -96,6 +97,7 @@ public function __construct(InputInterface $input, array $configuration = [])
OptionDefinition::PROGRESS,
OptionDefinition::LOG_JSON,
OptionDefinition::LOG_JUNIT,
+ OptionDefinition::LOG_SARIF,
OptionDefinition::WARNING,
OptionDefinition::OPTION_MEMORY_LIMIT,
OptionDefinition::IGNORE_EXIT_CODE,
diff --git a/src/Configuration/OptionDefinition.php b/src/Configuration/OptionDefinition.php
index 1751987e..aa7fda56 100644
--- a/src/Configuration/OptionDefinition.php
+++ b/src/Configuration/OptionDefinition.php
@@ -33,6 +33,7 @@ interface OptionDefinition
public const NO_PROGRESS = 'no-progress';
public const LOG_JSON = 'log-json';
public const LOG_JUNIT = 'log-junit';
+ public const LOG_SARIF = 'log-sarif';
public const IGNORE_EXIT_CODE = 'ignore-exit-code';
public const DEFAULT_JOBS = 5;
diff --git a/src/Configuration/OptionsFactory.php b/src/Configuration/OptionsFactory.php
index 43b9dcdf..8187a03b 100644
--- a/src/Configuration/OptionsFactory.php
+++ b/src/Configuration/OptionsFactory.php
@@ -54,6 +54,7 @@ protected function configureOptions(OptionsResolver $resolver): void
OptionDefinition::NO_PROGRESS => 'bool',
OptionDefinition::LOG_JSON => ['bool', 'null', 'string'],
OptionDefinition::LOG_JUNIT => ['bool', 'null', 'string'],
+ OptionDefinition::LOG_SARIF => ['bool', 'null', 'string'],
OptionDefinition::WARNING => 'bool',
OptionDefinition::OPTION_MEMORY_LIMIT => ['int', 'string'],
OptionDefinition::IGNORE_EXIT_CODE => 'bool',
@@ -89,5 +90,6 @@ protected function configureOptions(OptionsResolver $resolver): void
};
$resolver->setNormalizer(OptionDefinition::LOG_JSON, $outputFormat);
$resolver->setNormalizer(OptionDefinition::LOG_JUNIT, $outputFormat);
+ $resolver->setNormalizer(OptionDefinition::LOG_SARIF, $outputFormat);
}
}
diff --git a/src/Extension/OutputFormat.php b/src/Extension/OutputFormat.php
index 09e32ee6..08b76009 100644
--- a/src/Extension/OutputFormat.php
+++ b/src/Extension/OutputFormat.php
@@ -23,6 +23,7 @@
use Overtrue\PHPLint\Output\ConsoleOutput;
use Overtrue\PHPLint\Output\JsonOutput;
use Overtrue\PHPLint\Output\JunitOutput;
+use Overtrue\PHPLint\Output\SarifOutput;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -70,6 +71,8 @@ public function initFormat(ConsoleCommandEvent $event): void
$this->handlers[] = new JsonOutput(fopen($filename, 'w'));
} elseif (OptionDefinition::LOG_JUNIT == $name) {
$this->handlers[] = new JunitOutput(fopen($filename, 'w'));
+ } elseif (OptionDefinition::LOG_SARIF == $name) {
+ $this->handlers[] = new SarifOutput(fopen($filename, 'w'));
}
}
}
diff --git a/src/Output/SarifOutput.php b/src/Output/SarifOutput.php
new file mode 100644
index 00000000..51115c78
--- /dev/null
+++ b/src/Output/SarifOutput.php
@@ -0,0 +1,131 @@
+getFailures();
+
+ $driver = new ToolComponent('PHPLint');
+ $driver->setInformationUri('https://github.com/overtrue/phplint');
+ $driver->setVersion('9.1.0');
+
+ $tool = new Tool($driver);
+
+ $results = [];
+
+ foreach ($failures as $file => $failure) {
+ $result = new Result(new Message($failure['error']));
+
+ $artifactLocation = new ArtifactLocation();
+ $artifactLocation->setUri($this->pathToArtifactLocation($file));
+ $artifactLocation->setUriBaseId('WORKINGDIR');
+
+ $location = new Location();
+ $physicalLocation = new PhysicalLocation($artifactLocation);
+ $physicalLocation->setRegion(new Region($failure['line']));
+ $location->setPhysicalLocation($physicalLocation);
+ $result->addLocations([$location]);
+
+ $results[] = $result;
+ }
+
+ $run = new Run($tool);
+ $workingDir = new ArtifactLocation();
+ $workingDir->setUri($this->pathToUri(getcwd() . '/'));
+ $originalUriBaseIds = [
+ 'WORKINGDIR' => $workingDir,
+ ];
+ $run->addAdditionalProperties($originalUriBaseIds);
+ $run->addResults($results);
+
+ $log = new SarifLog([$run]);
+
+ $jsonString = json_encode(
+ $log,
+ JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT
+ );
+
+ $this->write($jsonString, true);
+ fclose($this->getStream());
+ }
+
+ /**
+ * Returns path to resource (file) scanned.
+ */
+ protected function pathToArtifactLocation(string $path): string
+ {
+ $workingDir = getcwd();
+ if ($workingDir === false) {
+ $workingDir = '.';
+ }
+ if (substr($path, 0, strlen($workingDir)) === $workingDir) {
+ // relative path
+ return substr($path, strlen($workingDir) + 1);
+ }
+
+ // absolute path with protocol
+ return $this->pathToUri($path);
+ }
+
+ /**
+ * Returns path to resource (file) scanned with protocol.
+ */
+ protected function pathToUri(string $path): string
+ {
+ if (parse_url($path, PHP_URL_SCHEME) !== null) {
+ // already a URL
+ return $path;
+ }
+
+ $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
+
+ // file:///C:/... on Windows systems
+ if (substr($path, 0, 1) !== '/') {
+ $path = '/' . $path;
+ }
+
+ return 'file://' . $path;
+ }
+}
\ No newline at end of file