chore: add Psalm static analysis (types, baseline, CI)

Adds psalm/plugin-laravel (Laravel-aware Psalm) at error level 8 with a
baseline so CI is green immediately. Type-analysis CI job with SHA-pinned
actions and an egress-hardened runner.
pull/6646/head
Alies Lapatsin 2 weeks ago
parent c8bed78bee
commit fe172ee044
No known key found for this signature in database

@ -0,0 +1,54 @@
# Generated by `psalm-laravel add github`, then trimmed to type analysis only.
# Psalm 6 runs type and taint analysis in separate modes. This workflow runs the type-analysis
# pass and fails on Psalm's exit code. Taint (security) scanning is available via
# `./vendor/bin/psalm --taint-analysis`; enable it as a second job once you are ready to triage
# its findings in a private channel (see the plugin README for the SARIF + Code Scanning setup).
name: Psalm
on:
push:
branches: [ dev ]
pull_request:
permissions:
contents: read
concurrency:
group: psalm-${{ github.ref }}
cancel-in-progress: true
jobs:
static-analysis:
name: Psalm type analysis
runs-on: ubuntu-latest
steps:
- name: Block unexpected egress
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
# Covers checkout, setup-php and Composer (public Packagist). Extend for other hosts
# (private registry, VCS repos, extra plugins). Use egress-policy: audit to discover them.
egress-policy: block
allowed-endpoints: >
api.github.com:443
codeload.github.com:443
github.com:443
packagist.org:443
release-assets.githubusercontent.com:443
repo.packagist.org:443
# No git ops after checkout, so drop the token: a compromised later step cannot push.
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.4'
coverage: none
- uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0
# No --report, so Psalm exits non-zero on findings and fails the job directly.
- name: Run Psalm (type analysis)
run: ./vendor/bin/psalm

@ -56,7 +56,8 @@
"laravel/telescope": "^5.5",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.8",
"pestphp/pest": "^4.0"
"pestphp/pest": "^4.0",
"psalm/plugin-laravel": "^3"
},
"autoload": {
"classmap": [

1835
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,282 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac">
<file src="app/Console/Commands/FixUsernames.php">
<NoValue>
<code><![CDATA[$u]]></code>
</NoValue>
</file>
<file src="app/FailedJob.php">
<PublicModelAccessor>
<code><![CDATA[getFailedAtAttribute]]></code>
</PublicModelAccessor>
</file>
<file src="app/Http/Controllers/AccountController.php">
<NoValue>
<code><![CDATA[return abort(403);]]></code>
<code><![CDATA[return abort(403);]]></code>
<code><![CDATA[return abort(403);]]></code>
<code><![CDATA[return abort(403);]]></code>
<code><![CDATA[return abort(406);]]></code>
<code><![CDATA[return abort(406);]]></code>
<code><![CDATA[return abort(406);]]></code>
<code><![CDATA[return abort(406);]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/Admin/AdminReportController.php">
<NoValue>
<code><![CDATA[return abort(403);]]></code>
</NoValue>
<UndefinedVariable>
<code><![CDATA[$meta]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
</UndefinedVariable>
</file>
<file src="app/Http/Controllers/AdminController.php">
<UndefinedClass>
<code><![CDATA[AdminProfile]]></code>
</UndefinedClass>
</file>
<file src="app/Http/Controllers/Api/AdminApiController.php">
<NoValue>
<code><![CDATA[return abort(403);]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/Api/ApiV1Controller.php">
<UndefinedVariable>
<code><![CDATA[$account]]></code>
</UndefinedVariable>
</file>
<file src="app/Http/Controllers/Api/ApiV1Dot1Controller.php">
<UndefinedDocblockClass>
<code><![CDATA[StatusTransformer]]></code>
</UndefinedDocblockClass>
</file>
<file src="app/Http/Controllers/Api/ApiV2Controller.php">
<UndefinedClass>
<code><![CDATA[DB]]></code>
<code><![CDATA[StatusService]]></code>
<code><![CDATA[StatusService]]></code>
<code><![CDATA[StatusService]]></code>
<code><![CDATA[UserFilterService]]></code>
<code><![CDATA[UserFilterService]]></code>
</UndefinedClass>
</file>
<file src="app/Http/Controllers/Api/V1/DomainBlockController.php">
<NoValue>
<code><![CDATA[return abort(500, 'Invalid domain or already blocked by server admins');]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/AppRegisterController.php">
<InaccessibleProperty>
<code><![CDATA[$token->token]]></code>
</InaccessibleProperty>
</file>
<file src="app/Http/Controllers/Groups/GroupsPostController.php">
<UndefinedVariable>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
<code><![CDATA[$status]]></code>
</UndefinedVariable>
</file>
<file src="app/Http/Controllers/Import/Instagram.php">
<NoValue>
<code><![CDATA[return abort(500);]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/PersonalAccessTokenController.php">
<InaccessibleProperty>
<code><![CDATA[$result->token]]></code>
<code><![CDATA[$result->token]]></code>
</InaccessibleProperty>
</file>
<file src="app/Http/Controllers/PortfolioController.php">
<UndefinedVariable>
<code><![CDATA[$metadata]]></code>
</UndefinedVariable>
</file>
<file src="app/Http/Controllers/ProfileController.php">
<NoValue>
<code><![CDATA[return abort(404);]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/RemoteAuthController.php">
<NoValue>
<code><![CDATA[$res]]></code>
<code><![CDATA[$res]]></code>
<code><![CDATA[$res]]></code>
<code><![CDATA[$res]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/ReportController.php">
<NoValue>
<code><![CDATA[return abort(400, 'Invalid report type');]]></code>
<code><![CDATA[return abort(400, 'Invalid report type');]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/StoryController.php">
<NoValue>
<code><![CDATA[return abort([], 403);]]></code>
</NoValue>
</file>
<file src="app/Http/Controllers/TimelineController.php">
<UndefinedClass>
<code><![CDATA[StatusTimelineTransformer]]></code>
</UndefinedClass>
</file>
<file src="app/Http/Kernel.php">
<UndefinedClass>
<code><![CDATA[CheckForAnyScope]]></code>
<code><![CDATA[CheckScopes]]></code>
</UndefinedClass>
</file>
<file src="app/Http/Middleware/Api/Admin.php">
<NoValue>
<code><![CDATA[return abort(403, 'You must be an administrator to do that');]]></code>
</NoValue>
</file>
<file src="app/Jobs/CuratedOnboarding/CuratedOnboardingNotifyAdminNewApplicationPipeline.php">
<NoValue>
<code><![CDATA[$adminUsernames]]></code>
<code><![CDATA[$ccAddresses]]></code>
</NoValue>
</file>
<file src="app/Jobs/GroupsPipeline/DeleteCommentPipeline.php">
<ParseError>
<code><![CDATA[)]]></code>
</ParseError>
</file>
<file src="app/Jobs/InboxPipeline/DeleteWorker.php">
<UndefinedVariable>
<code><![CDATA[$key]]></code>
</UndefinedVariable>
</file>
<file src="app/Jobs/RemoteFollowPipeline/RemoteFollowPipeline.php">
<UndefinedClass>
<code><![CDATA[Context]]></code>
<code><![CDATA[GuzzleHttpSignatures]]></code>
</UndefinedClass>
</file>
<file src="app/Jobs/ReportPipeline/AutospamNotifyAdminViaEmail.php">
<NoValue>
<code><![CDATA[$addresses]]></code>
<code><![CDATA[$addresses]]></code>
<code><![CDATA[$to]]></code>
</NoValue>
</file>
<file src="app/Jobs/ReportPipeline/ReportNotifyAdminViaEmail.php">
<NoValue>
<code><![CDATA[$addresses]]></code>
<code><![CDATA[$addresses]]></code>
<code><![CDATA[$to]]></code>
</NoValue>
</file>
<file src="app/Models/CustomFilter.php">
<PublicModelAccessor>
<code><![CDATA[getExpiresInAttribute]]></code>
<code><![CDATA[getFilterActionAttribute]]></code>
<code><![CDATA[getIrreversibleAttribute]]></code>
<code><![CDATA[getTitleAttribute]]></code>
<code><![CDATA[setFilterActionAttribute]]></code>
<code><![CDATA[setIrreversibleAttribute]]></code>
<code><![CDATA[setTitleAttribute]]></code>
</PublicModelAccessor>
<UndefinedDocblockClass>
<code><![CDATA[Collection]]></code>
</UndefinedDocblockClass>
</file>
<file src="app/Models/CustomFilterKeyword.php">
<PublicModelAccessor>
<code><![CDATA[setKeywordAttribute]]></code>
</PublicModelAccessor>
</file>
<file src="app/Observers/LikeObserver.php">
<UndefinedDocblockClass>
<code><![CDATA[\App\Models\Like]]></code>
<code><![CDATA[\App\Models\Like]]></code>
<code><![CDATA[\App\Models\Like]]></code>
<code><![CDATA[\App\Models\Like]]></code>
<code><![CDATA[\App\Models\Like]]></code>
</UndefinedDocblockClass>
</file>
<file src="app/Services/StoryIndexService.php">
<NoValue>
<code><![CDATA[$results]]></code>
</NoValue>
</file>
<file src="app/Util/ActivityPub/Helpers.php">
<UndefinedClass>
<code><![CDATA[UriException]]></code>
</UndefinedClass>
</file>
<file src="app/Util/Lexer/Autolink.php">
<UndefinedDocblockClass>
<code><![CDATA[autolinkActiveUsersOnly]]></code>
<code><![CDATA[protected $autolinkActiveUsersOnly = false;]]></code>
</UndefinedDocblockClass>
</file>
<file src="app/Util/Lexer/Regex.php">
<UnsupportedPropertyReferenceUsage>
<code><![CDATA[$re = &self::$patterns]]></code>
</UnsupportedPropertyReferenceUsage>
</file>
<file src="app/Util/RateLimit/User.php">
<PublicModelAccessor>
<code><![CDATA[getMaxCollectionsPerDayAttribute]]></code>
<code><![CDATA[getMaxCollectionsPerHourAttribute]]></code>
<code><![CDATA[getMaxCollectionsPerMonthAttribute]]></code>
<code><![CDATA[getMaxCommentsPerDayAttribute]]></code>
<code><![CDATA[getMaxCommentsPerHourAttribute]]></code>
<code><![CDATA[getMaxComposeMediaUpdatesPerDayAttribute]]></code>
<code><![CDATA[getMaxComposeMediaUpdatesPerHourAttribute]]></code>
<code><![CDATA[getMaxComposeMediaUpdatesPerMonthAttribute]]></code>
<code><![CDATA[getMaxHashtagFollowsPerDayAttribute]]></code>
<code><![CDATA[getMaxHashtagFollowsPerHourAttribute]]></code>
<code><![CDATA[getMaxInstanceBansPerDayAttribute]]></code>
<code><![CDATA[getMaxLikesPerDayAttribute]]></code>
<code><![CDATA[getMaxLikesPerHourAttribute]]></code>
<code><![CDATA[getMaxPostEditsPerDayAttribute]]></code>
<code><![CDATA[getMaxPostEditsPerHourAttribute]]></code>
<code><![CDATA[getMaxPostsPerDayAttribute]]></code>
<code><![CDATA[getMaxPostsPerHourAttribute]]></code>
<code><![CDATA[getMaxSharesPerDayAttribute]]></code>
<code><![CDATA[getMaxSharesPerHourAttribute]]></code>
<code><![CDATA[getMaxStoriesPerDayAttribute]]></code>
<code><![CDATA[getMaxStoriesPerHourAttribute]]></code>
<code><![CDATA[getMaxStoryDeletePerDayAttribute]]></code>
<code><![CDATA[getMaxUserBansPerDayAttribute]]></code>
</PublicModelAccessor>
</file>
<file src="config/websockets.php">
<UndefinedClass>
<code><![CDATA[\BeyondCode\LaravelWebSockets\API\FetchChannel]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\API\FetchChannels]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\API\FetchUsers]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\API\TriggerEvent]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\Apps\ConfigAppManager]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\ChannelManagers\LocalChannelManager]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\ChannelManagers\RedisChannelManager]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\Server\HealthHandler]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\Server\WebSocketHandler]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\Statistics\Collectors\MemoryCollector]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\Statistics\Collectors\RedisCollector]]></code>
<code><![CDATA[\BeyondCode\LaravelWebSockets\Statistics\Stores\DatabaseStore]]></code>
<code><![CDATA[\React\Promise\FulfilledPromise]]></code>
</UndefinedClass>
</file>
<file src="public/index.php">
<MissingFile>
<code><![CDATA[require __DIR__.'/../storage/framework/maintenance.php']]></code>
</MissingFile>
</file>
</files>

@ -0,0 +1,54 @@
<?xml version="1.0"?>
<psalm
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorLevel="8"
findUnusedCode="false"
ensureOverrideAttribute="false"
errorBaseline="psalm-baseline.xml"
>
<projectFiles>
<directory name="app"/>
<directory name="bootstrap"/>
<directory name="config"/>
<directory name="database"/>
<directory name="routes"/>
<!-- composer require psalm/plugin-phpunit psalm/plugin-mockery for the full tests support -->
<!-- <directory name="tests"/>-->
<file name="public/index.php"/>
<file name="artisan"/>
<ignoreFiles allowMissingFiles="true">
<directory name="bootstrap/cache"/>
<directory name="storage"/>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<plugins>
<!-- All Psalm Laravel options: https://github.com/psalm/psalm-plugin-laravel/blob/master/docs/config.md -->
<pluginClass class="Psalm\LaravelPlugin\Plugin">
<resolveDynamicWhereClauses value="true" />
<findMissingTranslations value="false" />
<findMissingViews value="false" />
</pluginClass>
</plugins>
<issueHandlers>
<ClassMustBeFinal errorLevel="info"/>
<ImplicitToStringCast errorLevel="info"/>
<MissingClosureReturnType errorLevel="info"/>
<MissingImmutableAnnotation errorLevel="info"/>
<MissingOverrideAttribute errorLevel="info"/>
<RedundantCast errorLevel="info"/>
<RedundantCondition errorLevel="info"/>
<UnnecessaryVarAnnotation errorLevel="suppress"/>
</issueHandlers>
<forbiddenFunctions>
<function name="var_dump" />
<function name="dd" />
</forbiddenFunctions>
</psalm>
Loading…
Cancel
Save