shieldci / analyzers-core
Shared foundation for building static analysis tools - includes abstract analyzer classes, result formatters, file parsers, and utilities
Requires
- php: ^8.1
- nikic/php-parser: ^5.6
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.40
- mockery/mockery: ^1.6
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v2.8.1
- v2.8.0
- v2.7.0
- v2.6.0
- v2.5.0
- v2.4.0
- v2.3.0
- v2.2.0
- v2.1.1
- v2.1.0
- v2.0.0
- v1.6.0
- v1.5.0
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.0
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.2
- v1.0.1
- v1.0.0
- v0.1.14
- v0.1.13
- v0.1.12
- v0.1.11
- v0.1.10
- v0.1.9
- v0.1.8
- v0.1.7
- v0.1.6
- v0.1.5
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1.0
- dev-fix/resolve-names-collecting-handler
This package is auto-updated.
Last update: 2026-10-06 14:59:06 UTC
README
Shared foundation for building static analysis tools. Includes abstract analyzer classes, result formatters, file parsers, and utilities.
Features
- Framework Agnostic: Works with any PHP 8.1+ project
- Type Safe: Full type hints and strict typing
- Extensible: Easy to create custom analyzers
- Well Tested: Comprehensive test suite (100% coverage)
- Modern PHP: Uses PHP 8.1+ features
- Laravel Compatible: Works with Laravel 9.x, 10.x, 11.x, 12.x and 13.x
Requirements
- PHP 8.1 or higher
- nikic/php-parser 5.6.x or any later 5.x (
^5.6) — 5.6 is the first release able to parse PHP 8.5, the newest version this package is tested against - Composer
Installation
composer require shieldci/analyzers-core
Architecture
Core Components
-
Interfaces
AnalyzerInterface- Contract for all analyzersResultInterface- Contract for analysis resultsReporterInterface- Contract for result formattersParserInterface- Contract for code parsersRecordingParserInterface- A parser that also keeps a readable log of what it could not parse
-
Abstract Base Classes
AbstractAnalyzer- Base class with timing, error handling, and helper methodsAbstractFileAnalyzer- Base class for file-based analyzers with file filtering
-
Value Objects
Location- Represents a code location (file, line, column)Issue- Represents a specific issue foundCodeSnippet- Represents a code snippet with context linesAnalyzerMetadata- Metadata about an analyzerParseFailure- A file that was handed to the parser but never produced an ASTParserCompatibility- Whether the default parser understands the running PHP
-
Results
AnalysisResult- Result of running a single analyzerResultCollection- Collection of analysis results
-
Utilities
AstParser- AST parsing using nikic/php-parserFileParser- File content parsing utilitiesCodeHelper- Code analysis helpersConfigFileHelper- Laravel configuration file path and line lookups (no AST)ConfigFileParser- Reads config files via AST, over a parser you keepPathHelper- Joining and relativising against a base path, with cross-platform separator normalizationMessageHelper- Error message sanitization (redacts credentials, tokens, IPs)InlineSuppressionParser- Parses@shieldci-ignoreinline suppression commentsPackageDetector- Detects installed Laravel packages from composer.lockFilamentPanelDetector- Detects a registered Filament panel provider, over a parser you keepPlatformDetector- Detects the hosting platform a run is executing onTraceHelper- Summarises exception traces as argument-free frames with relativised paths
-
Formatters
JsonFormatter- Format results as JSONConsoleFormatter- Format results for console output
Usage
Creating a Custom Analyzer
<?php use ShieldCI\AnalyzersCore\Abstracts\AbstractFileAnalyzer; use ShieldCI\AnalyzersCore\Contracts\ResultInterface; use ShieldCI\AnalyzersCore\ValueObjects\AnalyzerMetadata; use ShieldCI\AnalyzersCore\Enums\{Category, Severity}; class MySecurityAnalyzer extends AbstractFileAnalyzer { protected function metadata(): AnalyzerMetadata { return new AnalyzerMetadata( id: 'my-security-analyzer', name: 'My Security Analyzer', description: 'Checks for security vulnerabilities', category: Category::Security, severity: Severity::High, ); } protected function runAnalysis(): ResultInterface { $issues = []; foreach ($this->getPhpFiles() as $file) { $content = $this->readFile($file); // Find line number where eval() appears $lines = explode("\n", $content); foreach ($lines as $lineNum => $line) { if (str_contains($line, 'eval(')) { $issues[] = $this->createIssueWithSnippet( message: 'Dangerous eval() function found', filePath: $file, lineNumber: $lineNum + 1, severity: Severity::Critical, recommendation: 'Remove eval() and use safer alternatives', metadata: ['code' => 'dangerous-eval'] ); } } } if (empty($issues)) { return $this->passed('No security issues found'); } return $this->failed( 'Security issues detected', $issues, ['files_scanned' => count($this->getPhpFiles())] ); } }
Running an Analyzer
<?php $analyzer = new MySecurityAnalyzer(); $analyzer->setBasePath('/path/to/project'); $analyzer->setPaths(['src', 'app']); $result = $analyzer->analyze(); echo "Status: " . $result->getStatus()->value . PHP_EOL; echo "Message: " . $result->getMessage() . PHP_EOL; echo "Issues: " . count($result->getIssues()) . PHP_EOL;
Using Result Collection
<?php use ShieldCI\AnalyzersCore\Results\ResultCollection; $collection = new ResultCollection(); $collection->add($analyzer1->analyze()); $collection->add($analyzer2->analyze()); $collection->add($analyzer3->analyze()); echo "Score: " . $collection->score() . "%" . PHP_EOL; echo "Total Issues: " . $collection->totalIssues() . PHP_EOL; echo "Execution Time: " . $collection->totalExecutionTime() . "s" . PHP_EOL;
score() is the share of analyzers that returned a verdict and passed:
passed / (total - skipped). A skipped analyzer leaves the denominator entirely -
it is a deliberate waiver that produced no verdict, so it neither raises nor lowers the
score. An errored one stays in the denominator: an analyzer that could not run must
not read as a pass. A collection that is empty, or whose analyzers were all skipped,
scores 100. The formatters below report the same number from the same method.
Formatting Results
<?php use ShieldCI\AnalyzersCore\Formatters\{ConsoleFormatter, JsonFormatter}; $results = [$result1, $result2, $result3]; // Console output $consoleFormatter = new ConsoleFormatter(useColors: true, verbose: true); echo $consoleFormatter->format($results); // JSON output $jsonFormatter = new JsonFormatter(prettyPrint: true); $json = $jsonFormatter->format($results); file_put_contents('report.json', $json);
Using the AST Parser
<?php use ShieldCI\AnalyzersCore\Support\AstParser; use PhpParser\Node\Expr\MethodCall; $parser = new AstParser(); $ast = $parser->parseFile('/path/to/file.php'); // Find all method calls $methodCalls = $parser->findMethodCalls($ast, 'query'); // Find static calls $staticCalls = $parser->findStaticCalls($ast, 'DB', 'raw'); // Find nodes of specific type $classes = $parser->findNodes($ast, \PhpParser\Node\Stmt\Class_::class); // Resolve fully-qualified class names with NameResolver // After this, use $node->getAttribute('resolvedName') on Name nodes to get FQCNs $resolvedAst = $parser->resolveNames($ast); // With options — preserve original Name nodes, set only the 'resolvedName' attribute $resolvedAst = $parser->resolveNames($ast, ['replaceNodes' => false]); $fqcn = $someNameNode->getAttribute('resolvedName')?->toString(); // e.g. 'Illuminate\Database\Eloquent\Model'
Finding out what could not be parsed
parseFile() and parseCode() return an empty array when a file cannot be parsed, which
is indistinguishable from a file that parsed cleanly and contained nothing of interest. The
parser records each such file so a run can report them rather than passing them silently:
<?php use ShieldCI\AnalyzersCore\Enums\ParseFailureCause; use ShieldCI\AnalyzersCore\Support\AstParser; $parser = new AstParser(); $parser->resetFailures(); // once per run $parser->parseFile('/app/Broken.php'); foreach ($parser->failures() as $failure) { $failure->path; // '/app/Broken.php', or null if parseCode() got no origin $failure->line; // 6, or null when the parser reported no usable line $failure->message; // the parser's own message, or why the bytes were unreadable if ($failure->cause === ParseFailureCause::UnsupportedSyntax) { // Valid PHP here, but the pinned parser is older than the runtime. } }
The cause distinguishes who has to fix it:
| Cause | Meaning | Fixed by |
|---|---|---|
SyntaxError |
No PHP runtime would accept this file either | the author, repairing the code |
UnsupportedSyntax |
The running PHP accepts it; the pinned parser cannot read it | upgrading the toolchain |
Unreadable |
The file never reached a parser; its bytes could not be read | fixing the path or permissions |
Each file is recorded once, however many times it is parsed. resetFailures() is
deliberately separate from clearCache(): the AST cache is drained frequently to bound
memory, so a log tied to it would only ever hold the most recent caller's failures. The two
can be reset in any order: a failed parse is never cached, so after resetFailures() the next
parse of a file that still does not parse records it again.
A caller that recovered usable syntax from a file it could not parse can say so:
if ($parser->hasFailure($file) && $this->recoveredAnotherWay($file)) { $parser->recordRecovery($file); // true if there was a failure to annotate }
Ask hasFailure() rather than testing the AST. An empty file parses successfully to no
statements and records nothing, so branching on $ast === [] runs the fallback over every
empty file in a project. Spell the path as parseFile() was given it, or as parseCode()
was given its origin — records are keyed on that string and neither side normalises, so a
second spelling reports false rather than matching.
The failure itself stands. The log is keyed per file and shared by every caller, so the
record under a path may belong to a different caller that really did skip the file; and an
error-recovering parser returns the statements it could rebuild and drops the broken region,
so the file is not fully analysed even for the caller that recovered it. Removing the record
would trade a false skip for a false pass. recoveries() returns the annotated paths, always
a subset of failures(), so a reporter can say "would not parse, partially recovered".
When parsing generated code, pass the real source path as the origin and a translator for the line. The translator runs only if the parse fails, since the failing line is not known until then:
$parser->parseCode($compiled, '/resources/views/page.blade.php', fn (int $line) => $lineMap[$line] ?? $line);
Reaching failures from the config and package helpers
Three helpers in this package read an AST, and all three answer with an absence: no such key, no such nested key, no registered panel provider. A file that would not parse produces the same absence, so a caller that cannot tell them apart reports a broken config as a config that simply does not set the thing.
Each lives on a class you construct with a parser, and afterwards that parser is the record:
$parser = new AstParser(); $config = (new ConfigFileParser($parser))->parseArray($configPath); if ($parser->hasFailure($configPath)) { // The config could not be parsed -- not "defines no keys". }
| Instead of | Use |
|---|---|
ConfigFileHelper::parseConfigArray($path) |
(new ConfigFileParser($parser))->parseArray($path) |
ConfigFileHelper::findNestedArrayKeyLine($path, $parent, $child) |
(new ConfigFileParser($parser))->findNestedArrayKeyLine($path, $parent, $child) |
PackageDetector::isFilamentConfigured($basePath) |
(new FilamentPanelDetector($parser))->isConfigured($basePath) |
The three statics still exist and keep the same signatures, so released callers keep compiling.
One of them changes its answer: PackageDetector::isFilamentConfigured() delegates to
FilamentPanelDetector, so an application whose panel provider does not parse now comes back
true where it came back false — the bug this release fixes, and the reason the static cannot
tell you that is what happened. All three are @deprecated and will go in 3.0.0: each builds
its own parser and drops it, which is the behaviour this section exists to replace.
Both classes take a RecordingParserInterface, not an AstParser, so you can pass the parser
your application already holds — including one resolved from a container.
Four things worth knowing:
isConfigured()recovers a provider that will not parse. A file the parser could not read has its class name taken straight out of the source, so a broken provider does not silently become "no Filament here". The failure record stands and the path is added torecoveries(), sotruewith a non-emptyrecoveries()means "configured, and one of those files is broken".- It reports failures only partially, by design. A file is parsed only once it contains both
extendsandPanelProvider. A provider broken badly enough to lose either string is rejected before the parser sees it, and leaves no record. A parser that throws rather than records leaves none either — nothing reaches its failure log, so there is nothing forrecordRecovery()to attach to.AstParserrecords, so this only affects an implementation of your own. - The recovery reads code, not comments. Comments and string literals are stripped before the class name is matched, so a commented-out provider or one quoted in a generator template is not mistaken for a live declaration.
- A parser you keep, keeps what it read.
AstParsercaches the AST of each file that parsed, so a parser threaded through a whole run holds every AST that run touched. That is the point — it is what makes the log readable — but it is also memory.clearCache()drops the ASTs, andresetFailures()drops the failure and recovery logs. They are separate on purpose: the cache is usually drained per analyzer to bound memory, while the log is run-scoped and belongs to whatever orchestrates the run. Neither has to precede the other: a failed parse is not cached, so resetting the log never hides a file that still does not parse.
Is the parser older than the PHP you are running?
AstParser targets the newest PHP the installed nikic/php-parser supports. When a project runs
on a newer PHP than that, nothing throws — every file using current syntax quietly becomes a
ParseFailure with cause UnsupportedSyntax instead of an AST, and a whole project can be
reported clean because nothing in it was read. UnsupportedSyntax only says so once a file has
tripped it, one entry per file, for what is really a single toolchain problem.
Ask directly instead:
use ShieldCI\AnalyzersCore\Support\AstParser; $compatibility = AstParser::compatibility(); if (! $compatibility->isSupported()) { // "php-parser understands 8.5, but this is running on 8.6" printf( 'php-parser understands %s, but this is running on %s', $compatibility->parserVersion(), $compatibility->runtimeVersion() ); } $compatibility->toArray(); // ['supported' => false, 'parser_version' => '8.5', 'runtime_version' => '8.6']
Check it once per run and report it beside the failure list — the fix is a toolchain upgrade
(composer update nikic/php-parser), not a code change, so it belongs next to the run rather
than blamed on a file.
This is data with no message, like ParseFailureCause: what to say about it is the consuming
package's decision. It is static and describes the parser this package builds by default —
a parser you injected yourself is a deliberate choice, so it is never reported as a mismatch.
Using Code Helpers
<?php use ShieldCI\AnalyzersCore\Support\CodeHelper; $code = file_get_contents('/path/to/file.php'); // Calculate complexity $complexity = CodeHelper::calculateComplexity($code); // Find dangerous functions $dangerous = CodeHelper::findDangerousFunctions($code); // Check if looks like SQL $isSql = CodeHelper::looksLikeSql($string); // Validate naming conventions $isValid = CodeHelper::isValidClassName('MyClass');
Using Code Snippets
The CodeSnippet value object provides rich code context for issues with several advanced features:
<?php use ShieldCI\AnalyzersCore\ValueObjects\CodeSnippet; // Create a code snippet from a file $snippet = CodeSnippet::fromFile( filePath: '/path/to/file.php', targetLine: 42, contextLines: 8 // Lines before/after to show (default: 8) ); if ($snippet !== null) { // Get the lines with line numbers as keys $lines = $snippet->getLines(); // Get the target line number $targetLine = $snippet->getTargetLine(); // Get file path $filePath = $snippet->getFilePath(); // Convert to array for serialization $array = $snippet->toArray(); }
Advanced Features:
-
Smart Context Expansion
- Automatically detects method/class signatures above the target line
- Expands context to include signature if within 15 lines
- Provides crucial context for understanding where issues occur
- Detects: classes, interfaces, traits, enums, public/protected/private methods
-
Configurable Context
- Default: 8 lines before and after target line
- Customizable via
contextLinesparameter - Automatically handles file boundaries
-
Line Truncation
- Truncates long lines to 250 characters to prevent terminal wrapping
- Preserves readability in console output
-
Null Safety
- Returns
nullif file doesn't exist or can't be read - Graceful error handling for runtime exceptions
- Returns
Example with Issue:
<?php use ShieldCI\AnalyzersCore\ValueObjects\{Issue, Location, CodeSnippet}; use ShieldCI\AnalyzersCore\Enums\Severity; $issue = new Issue( message: 'Hardcoded credentials detected', location: new Location('/path/to/file.php', 42), severity: Severity::Critical, recommendation: 'Move credentials to environment variables', metadata: ['type' => 'password', 'code' => 'hardcoded-credentials'], codeSnippet: CodeSnippet::fromFile('/path/to/file.php', 42) ); // The code snippet is displayed in verbose console output with: // - Line numbers alongside each line // - "→" prefix on the target line, " " prefix on context lines // - Indentation preserved (trailing whitespace stripped, leading kept)
Smart Context Expansion Example:
// Given this code: // Line 35: public function processPayment($amount) // Line 36: { // Line 37: // validation // Line 38: // ... // Line 42: $hardcodedKey = 'secret123'; // ← Issue here // Line 43: } // Even though line 35 is normally outside the 8-line context window, // CodeSnippet automatically includes it because it's the method signature $snippet = CodeSnippet::fromFile('/path/to/file.php', 42); // Result includes lines 35-50 (method signature + context) // instead of just lines 34-50
Using Config File Helper
The ConfigFileHelper utility provides powerful methods for working with Laravel configuration files, particularly useful for analyzers that need to report issues in config files with precise line numbers.
<?php use ShieldCI\AnalyzersCore\Support\ConfigFileHelper; // Get the path to a config file $configPath = ConfigFileHelper::getConfigPath( basePath: '/path/to/project', file: 'database', // with or without .php extension fallback: fn($file) => config_path($file) // Optional Laravel helper fallback ); // Result: /path/to/project/config/database.php // Find the line number where a specific key is defined $lineNumber = ConfigFileHelper::findKeyLine( configFile: '/path/to/project/config/database.php', key: 'default' ); // Returns the line number (1-indexed) where 'default' => is defined // Find a key within a parent array $lineNumber = ConfigFileHelper::findKeyLine( configFile: '/path/to/project/config/database.php', key: 'driver', parentKey: 'connections' // Search within 'connections' array ); // Returns the line number where 'driver' => is defined within 'connections' // Find a nested key within a specific array item $lineNumber = ConfigFileHelper::findNestedKeyLine( configFile: '/path/to/project/config/cache.php', parentKey: 'stores', nestedKey: 'driver', nestedValue: 'redis' // Search within 'redis' store configuration ); // Returns the line number where 'driver' => is defined // within the 'redis' item in the 'stores' array
Advanced Features:
-
Comment-Aware Searching
- Automatically strips single-line comments (
//,#) - Avoids false positives from commented-out config
- Automatically strips single-line comments (
-
Precise Pattern Matching
- Uses regex to match exact array key patterns:
'key' =>or"key" => - Handles various spacing:
'key'=>or'key' => - Avoids matching keys in string values or comments
- Uses regex to match exact array key patterns:
-
Nested Array Navigation
- Can search within parent arrays using
parentKeyparameter - Detects when entering/exiting parent array boundaries
- Handles nested array structures like connections, stores, etc.
- Can search within parent arrays using
-
Smart Indentation Detection
- Uses indentation level to determine array nesting
- Stops searching when encountering top-level keys outside target scope
- Prevents false matches in unrelated config sections
-
Fallback Support
- Returns line 1 if key not found (safe default)
- Supports optional Laravel
config_path()fallback for non-Laravel environments
Use Cases:
- Database Analyzers: Find connection settings, driver configurations
- Cache Analyzers: Locate cache store configurations, driver settings
- Session Analyzers: Find session driver, lifetime, security settings
- Queue Analyzers: Locate queue connection, driver configurations
- Mail Analyzers: Find mail driver, encryption settings
Parsing Config Arrays
ConfigFileParser::parseArray() parses a PHP config file that returns an array and extracts the top-level key–value pairs via AST — no regex, no fragile text matching.
An empty result means both "defines no string keys" and "would not parse". Hold on to the parser you construct it with and the two stop looking alike — see Reaching failures from the config and package helpers.
<?php use ShieldCI\AnalyzersCore\Support\AstParser; use ShieldCI\AnalyzersCore\Support\ConfigFileParser; $parser = new AstParser(); $entries = (new ConfigFileParser($parser))->parseArray('/path/to/config/session.php'); if ($entries === [] && $parser->hasFailure('/path/to/config/session.php')) { // Unreadable, not empty. Do not report "no keys set". } // Each entry has: value, line, isEnvCall, envDefault, envHasDefault foreach ($entries as $key => $entry) { echo "{$key} on line {$entry['line']}: "; if ($entry['isEnvCall']) { // Value comes from env() at runtime echo "env() call"; if ($entry['envHasDefault']) { var_dump($entry['envDefault']); // The default argument value } } else { var_dump($entry['value']); // Literal: string, int, float, bool, or null } }
Example — checking session cookie security:
$session = (new ConfigFileParser($parser))->parseArray($configPath); // Resolve the effective value (literal or env() default) $secure = $session['secure']['isEnvCall'] ? $session['secure']['envDefault'] : $session['secure']['value']; if ($secure !== true) { $issues[] = $this->createIssueWithSnippet( message: 'Session cookies are not marked secure', filePath: $configPath, lineNumber: $session['secure']['line'], severity: Severity::High, recommendation: 'Set SESSION_SECURE_COOKIE=true in production', ); }
Supported value types:
| PHP Source | value |
|---|---|
'string' |
'string' |
42 / 3.14 |
42 / 3.14 |
true / false / null |
true / false / null |
PHP_INT_MAX (constant) |
'PHP_INT_MAX' (string) |
env('KEY') |
null (isEnvCall = true) |
env('KEY', 'default') |
null (envDefault = 'default') |
['nested', 'array'] |
null (complex, not extracted) |
Stripping PHP Comments
FileParser::stripAllComments() removes all PHP comment styles from source code using the tokenizer — correctly handling comments inside strings, URLs, docblocks, and multiline blocks.
<?php use ShieldCI\AnalyzersCore\Support\FileParser; $code = file_get_contents('/path/to/file.php'); // Strips //, #, /* */, and /** */ comments // Preserves line numbering (removed lines replaced with blank lines) // Safe for URLs in strings: "https://example.com" is NOT stripped $stripped = FileParser::stripAllComments($code);
Unlike FileParser::stripComments() (which works on single lines via regex and breaks on URLs), stripAllComments() uses token_get_all() and handles arbitrary PHP source correctly.
Sanitizing Error Messages
MessageHelper::sanitizeErrorMessage() redacts sensitive values from error messages before they appear in analyzer recommendations — preventing credentials and tokens from leaking into reports.
<?php use ShieldCI\AnalyzersCore\Support\MessageHelper; // Redacts: passwords, API keys, bearer tokens, AWS keys, internal IPs (10.x, 172.x, 192.168.x) $safe = MessageHelper::sanitizeErrorMessage( 'Connection failed: password=s3cr3t host=10.0.0.5' ); // → 'Connection failed: password=*** host=***.*.*.*' // Use in analyzer recommendations: $issues[] = $this->createIssue( message: 'Redis connection failed', location: null, severity: Severity::High, recommendation: MessageHelper::sanitizeErrorMessage($e->getMessage()), );
Redacted patterns:
| Pattern | Replacement |
|---|---|
password=…, passwd=…, pwd=…, pass=… |
password=*** |
api_key=…, auth_token=… |
api_key=*** |
secret=…, client_secret=…, private_key=… |
secret=*** |
token=… |
token=*** |
Bearer <token> |
bearer *** (the keyword is lowercased) |
AKIA… (AWS access key) |
AKIA*** |
scheme://user:pass@host |
scheme://***:***@host |
10.x.x.x, 172.16–31.x.x, 192.168.x.x |
***.*.*.* |
This is a denylist over free-form text, so treat it as defence in depth rather than a guarantee:
it matches on a key (password=), so a bare positional value in a message is not recognised.
The private-IP rule is deliberately broad and will rewrite any matching dotted quad — a message
reading MySQL 10.5.8.1 is not supported comes back as MySQL ***.*.*.* is not supported.
Where the data is structured rather than free-form, prefer dropping the sensitive fields outright.
TraceHelper::frames() below is the worked example.
Error Result Metadata
When an analyzer throws, AbstractAnalyzer::analyze() catches it and returns a Status::Error
result instead of letting the exception escape. Two metadata keys describe what happened:
$result = $analyzer->analyze(); $result->getStatus(); // Status::Error $result->getMessage(); // 'Analysis failed: <redacted exception message>' $result->getMetadata(); // [ // 'exception' => 'RuntimeException', // 'trace' => [ // ['file' => 'app/Db.php', 'line' => 42, 'function' => 'connect', 'class' => 'App\\Db'], // ['file' => 'app/MyAnalyzer.php', 'line' => 18, 'function' => 'runAnalysis', 'class' => 'App\\MyAnalyzer'], // ], // ]
trace is built by TraceHelper::frames(), which keeps exactly those four keys per frame:
- No
args. PHP'sgetTraceAsString()renders every frame's arguments, truncating strings at 15 characters — so a credential shorter than that appeared in full. This value is written to the JSON report and uploaded to the platform, so nothing caller-supplied may ride along. - No
object. A raw frame also carries the live receiver.getTraceAsString()never rendered it, which makes it easy to forget, butjson_encode()would serialise its public properties. - Base-relative paths, matching how
Issuelocations are reported. A frame outside the base path keeps its original absolute spelling. - At most
TraceHelper::MAX_FRAMES(20) frames, so the payload is bounded.
The message is passed through MessageHelper::sanitizeErrorMessage() with a 500-character cap.
TraceHelper is usable directly wherever you catch something you intend to report:
<?php use ShieldCI\AnalyzersCore\Support\TraceHelper; try { $this->connect($dsn, $user, $password); } catch (Throwable $e) { $frames = TraceHelper::frames($e, base_path()); // Or keep fewer frames still: $shallow = TraceHelper::frames($e, base_path(), 5); }
Parsing Inline Suppressions
InlineSuppressionParser parses // @shieldci-ignore comments to determine whether a given line should suppress a specific analyzer rule.
<?php use ShieldCI\AnalyzersCore\Support\InlineSuppressionParser; $parser = new InlineSuppressionParser(); $lines = file('/path/to/file.php', FILE_IGNORE_NEW_LINES); // Returns true if the line (or an adjacent suppression comment) suppresses $analyzerId $suppressed = $parser->isLineSuppressed($lines, $lineNumber, 'sql-injection'); if (! $suppressed) { $issues[] = $this->createIssueWithSnippet(/* … */); }
Supported suppression styles:
// Same-line: $query = $db->raw($input); // @shieldci-ignore sql-injection // Previous-line: // @shieldci-ignore sql-injection $query = $db->raw($input); // Suppress multiple rules: // @shieldci-ignore sql-injection, xss echo $userInput; // Suppress all rules on a line: // @shieldci-ignore echo $trustedContent; /** * @shieldci-ignore sql-injection */ $query = $db->raw($input);
Enums
ShieldCI Analyzers Core provides four enums with rich helper methods for better developer experience.
The fourth, ParseFailureCause, is covered above under Finding out what could not be parsed.
Status
Represents the result status of an analyzer execution.
Cases:
Status::Passed- Analysis completed successfully with no issuesStatus::Failed- Analysis found critical issues that need fixingStatus::Warning- Analysis found warnings that should be reviewedStatus::Skipped- Analysis was skipped (not applicable)Status::Error- Analysis encountered an error during execution
Category
Represents the category/type of an analyzer.
Cases:
Category::Security- Security vulnerabilities and risks (🔒)Category::Performance- Performance issues and optimizations (⚡)Category::CodeQuality- Code quality and maintainability (📊)Category::BestPractices- Best practices and conventions (✨)Category::Reliability- Reliability and stability issues (🛡️)
Severity
Represents the severity level of an issue.
Cases:
Severity::Critical- Critical security or stability issue requiring immediate attentionSeverity::High- High priority issue that should be addressed soonSeverity::Medium- Medium priority issue that should be consideredSeverity::Low- Low priority issue or minor improvementSeverity::Info- Informational message or suggestion
Testing
# Run tests composer test # Run tests with coverage composer test-coverage # Run static analysis composer analyse # Format code composer format
Directory Structure
src/
├── Contracts/ # Interfaces
├── Abstracts/ # Abstract base classes
├── Enums/ # Enum types
├── ValueObjects/ # Immutable value objects
├── Results/ # Result classes
├── Support/ # Utility classes
└── Formatters/ # Result formatters
tests/
├── Unit/ # Unit tests
Used By
- shieldci/laravel - Free Laravel analyzer package
- shieldci/laravel-pro - Pro Laravel analyzer package
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License. See LICENSE file for details.
Credits
Built by the ShieldCI team.