Skip to content

feat(plugins): add preserve_autoload option for PHP plugin configuration#2744

Draft
sinemacula-ben wants to merge 2 commits intoqltysh:mainfrom
sinemacula-ben:feat/preserve-autoload-option
Draft

feat(plugins): add preserve_autoload option for PHP plugin configuration#2744
sinemacula-ben wants to merge 2 commits intoqltysh:mainfrom
sinemacula-ben:feat/preserve-autoload-option

Conversation

@sinemacula-ben
Copy link
Copy Markdown

@sinemacula-ben sinemacula-ben commented Apr 3, 2026

Summary

  • Adds preserve_autoload boolean option to plugin configuration (defaults to false, preserving existing behavior)
  • When enabled, retains autoload/autoload-dev sections in the sandbox's composer.json and creates symlinks from the sandbox to the workspace root so Composer's generated autoloader resolves PSR-4/classmap/files paths to actual project source files
  • Adds workspace_root (serde-skipped) to PluginDef so symlinks target the correct project root even when package_file lives in a subdirectory like .qlty/configs/
  • Adds config validation: preserve_autoload = true requires package_file to be set

Problem

filter_composer unconditionally strips autoload/autoload-dev from the user's composer.json before installing in the sandbox. This prevents PHPStan extensions like Larastan from working — Larastan bootstraps the Laravel application at startup and needs the project's PSR-4 namespace mappings (e.g., App\app/) in the sandbox autoloader. Without them, Larastan crashes with Class "App\..." not found.

Solution

The symlink approach was chosen as the least invasive fix:

  1. preserve_autoload = true keeps autoload sections in the filtered composer.json
  2. Before composer update, symlinks are created from {sandbox}/{path}{workspace_root}/{path} for each autoload path
  3. Composer's generated autoloader uses $baseDir (the sandbox) to resolve paths — symlinks make these paths valid
  4. Symlinks are created before composer update so Composer's classmap scanner can also find classes

Usage

[[plugin]]
name = "phpstan"
version = "2.1.38"
package_file = ".qlty/configs/composer.json"
package_filters = ["phpstan", "larastan"]
preserve_autoload = true

Files changed

File Change
qlty-config/src/config/plugin.rs preserve_autoload field on PluginDef and EnabledPlugin, workspace_root field on PluginDef, validation
qlty-check/src/planner/config.rs Wires preserve_autoload and workspace_root from config to runtime
qlty-check/src/tool/php/composer.rs Conditional autoload removal, create_autoload_symlinks, collect_autoload_paths, platform-specific create_symlink, tests

Test plan

  • test_filter_composer — existing behavior unchanged (autoload stripped by default)
  • test_filter_composer_no_filter — existing behavior unchanged
  • test_filter_composer_preserve_autoload — autoload retained when flag is set
  • test_collect_autoload_paths — all Composer autoload types extracted (psr-4, psr-0, classmap, files)
  • test_collect_autoload_paths_empty — no autoload sections returns empty
  • test_collect_autoload_paths_skips_empty_strings — root namespace mapping ("") skipped
  • test_create_autoload_symlinks — symlinks created from sandbox to workspace root (composer.json in subdirectory)
  • test_create_autoload_symlinks_skips_when_disabled — no symlinks when preserve_autoload is false
  • test_create_autoload_symlinks_skips_nonexistent_targets — missing source directories gracefully skipped
  • test_enabled_plugin_validate_success_with_preserve_autoload_and_package_file — valid config accepted
  • test_enabled_plugin_validate_failure_with_preserve_autoload_but_no_package_file — invalid config rejected
  • Verify existing test_update_existing_composer_json, test_lock_file_* tests still pass
  • Manual test with PHPStan + Larastan on a Laravel project

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

When PHP linter extensions like Larastan bootstrap the Laravel
application, they need the project's PSR-4 namespace mappings in the
sandbox autoloader. Previously, filter_composer unconditionally stripped
autoload/autoload-dev sections, causing "Class not found" crashes.

This adds a preserve_autoload boolean (default false) that, when
enabled, preserves autoload sections and creates symlinks from the
sandbox to the project root so Composer's generated autoloader resolves
paths to the actual source files.
@sinemacula-ben sinemacula-ben force-pushed the feat/preserve-autoload-option branch from dac181b to 03d6eef Compare April 3, 2026 16:08
@qltysh
Copy link
Copy Markdown
Contributor

qltysh bot commented Apr 3, 2026

1 new issue

Tool Category Rule Count
qlty Structure High total complexity (count = 53) 1

Ok(())
});
}
}
Copy link
Copy Markdown
Contributor

@qltysh qltysh bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High total complexity (count = 53) [qlty:file-complexity]

@sinemacula-ben sinemacula-ben force-pushed the feat/preserve-autoload-option branch from 02c2f7b to 208c1c8 Compare April 3, 2026 17:03
Extract collect_psr_paths and collect_string_array_paths helpers to
address qlty nested-control-flow and function-complexity findings.
@sinemacula-ben sinemacula-ben force-pushed the feat/preserve-autoload-option branch from 208c1c8 to fd37bad Compare April 3, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants