DDEV Drupal Code Quality
Overview
This add-on installs a local code quality tool suite for Drupal projects and IDE usage, starting from Drupal.org GitLab CI template defaults. It provides DDEV commands and host shims so developers can run the same checks locally that GitLab CI runs on Drupal.org.
DDEV add-on that installs local code quality tooling based on Drupal.org GitLab CI template defaults (PHPStan, PHPCS, ESLint, Stylelint, Prettier, CSpell) for local CLI and IDE usage.
Tools covered:
- PHPStan
- PHPCS / PHPCBF
- ESLint
- Stylelint
- Prettier
- CSpell
- Composer validate
- php-parallel-lint (when installed)
Installation
# Install from GitHub
ddev add-on get UltraBob/ddev-drupal-code-quality
ddev restart
# Or, for local development
ddev add-on get /path/to/ddev-drupal-code-quality
ddev restart
Related add-ons and overlap
There is overlap with other Drupal-focused DDEV add-ons. The key difference is the project type each one is designed for.
| Add-on | Best for | Typical project layout |
|---|---|---|
UltraBob/ddev-drupal-code-quality |
Full Drupal website projects where your site repo already contains Drupal code and custom code. | Existing site/project repo; installs code-quality configs and IDE shims in-place. |
ddev/ddev-drupal-contrib |
Drupal contrib module/theme development where the contrib project is the center of the repo. | Contrib project repo with Drupal scaffolded around it (symlink workflow). |
justafish/ddev-drupal-core-dev / joachim-n/ddev-drupal-core-dev |
Drupal core development. | Drupal core checkout or core-dev project template. |
Practical guidance
- Use the add-on that matches your project type (full-site, contrib, or core).
- These add-ons overlap in commands/configs and are generally not intended to be combined in one project.
- If you are developing contrib modules/themes, prefer
ddev/ddev-drupal-contrib. - If you are developing Drupal core, prefer
ddev-drupal-core-dev(see above forks). - If you are working on a full website project and want local checks aligned with Drupal.org GitLab template defaults, use this add-on.
Repository structure
commands/: DDEV web commands copied into project.ddev/commands.drupal-code-quality/: project-root configs and.ddevshims copied by the installer.dcq-install.sh: conflict-aware installer invoked byinstall.yaml.install.yaml: DDEV add-on install definition.
During installation, the add-on copies Drupal.org GitLab CI template default config files into the project root. If conflicts are detected, you can choose to back up and replace, skip, or abort. Skipping a config may diverge from the Drupal.org GitLab CI template defaults. The installer will prompt for:
- A pre-prompt summary of recommended defaults.
- Accept recommended settings (default: yes). Press Enter to apply the recommended defaults without further prompts.
- Conflict handling (default: skip unless you choose replace/abort).
- PHP tooling dependencies (install
drupal/core-devor runddev composer install). - PHPStan default level (keep GitLab CI template level 0 or choose a local level 0-10; recommend 3).
- Node toolchain install in the project root and package manager selection.
- Missing Drupal JS dependencies when a root
package.jsonexists. - Optional
.gitignoreupdate fordcq-reports/(default: yes). - IDE settings (merge/overwrite/skip when templates are available). The installer runs in bash so it does not require host PHP.
Recommended settings apply these defaults without further prompts:
replace conflicts (with backups), install PHP dev tools, install JS deps in the
project root, set PHPStan level 3, merge IDE settings, and add dcq-reports/
to .gitignore. Non-interactive runs with no overrides apply the recommended
settings automatically.
If PHPStan/PHPCS/PHPCBF binaries are missing, the installer prompts to add
drupal/core-dev (or to run ddev composer install if it is already required).
It uses ddev composer require --with-all-dependencies to avoid lockfile
conflicts.
Usage
For CLI usage, prefer the DDEV commands:
ddev phpstan
ddev phpcs
ddev phpcbf
ddev eslint
ddev stylelint
ddev prettier
ddev cspell
ddev composer-validate
ddev checks
Host shims are installed under .ddev/drupal-code-quality/tooling/bin. These are intended for
IDE tool paths or tools that require a local binary path:
./.ddev/drupal-code-quality/tooling/bin/phpstan
./.ddev/drupal-code-quality/tooling/bin/phpcs
./.ddev/drupal-code-quality/tooling/bin/eslint
./.ddev/drupal-code-quality/tooling/bin/stylelint
./.ddev/drupal-code-quality/tooling/bin/prettier
./.ddev/drupal-code-quality/tooling/bin/cspell
./.ddev/drupal-code-quality/tooling/bin/checks
Fix commands
ddev eslint-fix, ddev prettier-fix, and ddev stylelint-fix apply formatting changes directly, matching the behavior of their underlying tools (eslint --fix, prettier --write, stylelint --fix). All three commands support an optional --preview flag that builds a patch preview (saved to dcq-reports/), displays it, and prompts Apply these changes? [y/N] before applying. Use --preview when you want to review changes before committing them.
IDE settings (VS Code/Codium)
Starter settings live in .ddev/drupal-code-quality/ide-settings/vscode. During install, you can
choose to merge them into .vscode/settings.json and
.vscode/extensions.json, back up and overwrite, or skip and handle them
manually.
The template points PHP tooling at .ddev/drupal-code-quality/tooling/bin and JS tooling at local
node_modules. Override the paths if you prefer a different location.
Note that VS Code does not install the recommended extensions automatically.
The first time you open the workspace after install, VS Code shows a
notification that this workspace has extension recommendations — click
Show Recommendations and install the listed extensions so the IDE
integration works. If you dismissed the notification, open the Extensions view
and filter by @recommended to find them.
Requirements
- DDEV project with Drupal core in the configured docroot (default
web/). The installer records the docroot in.ddev/.dcq-docrootfor wrappers. - Composer dependencies installed (
ddev composer install). - Node toolchain for JS linting (npm or yarn; the installer selects based on
lockfiles and can create a root
package.jsonfrom Drupal core when missing).- If you use yarn, enable corepack in DDEV.
Configuration notes
- Reports:
dcq-reports/is created at the project root when runningchecksor the*-fixcommands (logs + patch previews).- Add
dcq-reports/to.gitignoreif you do not want to track it.
- ESLint toolchain: Node tooling is installed at the project root only
(see
docs/decisions/node-deps-root-only-2026-01-22.md). All wrappers resolve tools fromnode_modules/at the project root. - ESLint config mode:
ESLINT_CONFIG_MODE=nearest(default) groups by nearest config file.ESLINT_CONFIG_MODE=fixedprefers.eslintrc.passing.json, then.eslintrc.jsonin the project root.
- ESLint warning visibility (GitLab CI parity):
DCQ_ESLINT_QUIET=1(default) adds--quiettoddev eslintandddev eslint-fix, so warnings are suppressed.- Set
DCQ_ESLINT_QUIET=0to include warnings in CLI output. Persist this in.ddev/config.yaml(or.ddev/config.yml):web_environment: - DCQ_ESLINT_QUIET=0 - VS Code uses its own setting for extension diagnostics. Set
"eslint.quiet": falsein.vscode/settings.jsonto include warnings in the IDE. - Installer behavior:
overwriteregenerates IDE settings from template;mergeonly adds missing keys and will not change an existingeslint.quietvalue.
- CSpell parity:
- Run
ddev exec php /mnt/ddev_config/drupal-code-quality/tooling/scripts/prepare-cspell.php -s .preparedonce and replace.cspell.jsonafter reviewing the diff. ddev cspelldefaults to scanning.when no paths are passed; scope is controlled by.cspell.json(especiallyignorePaths)..cspell-project-words.txtis created by the installer (empty) and updated byddev cspell-suggestwhen you accept suggested words.
- Run
- ESLint / Stylelint / Prettier default scope:
- These wrappers default to scanning the configured docroot when no paths are passed.
- Scope/exclusions are controlled by visible config files:
.eslintignore,.stylelintignore, and.prettierignore. - Installer appends DCQ defaults to
.prettierignoreso the file remains the single source of truth for Prettier scope.
- PHPCS / PHPCBF default scope:
- When a project
.phpcs.xmlis installed by the add-on,ddev phpcsandddev phpcbfwith no path default to scanningmodules,themes,profiles, andsitesunder the docroot (the same scope as PHPStan). - The generated ruleset excludes
**/contrib/**,**/node_modules/**,__DOCROOT__/themes/blank/**(the starter theme Drupal CMS site templates generate directly underthemes/), and__DOCROOT__/sites/*/files/**. - You can still pass explicit paths to narrow runs.
- When a project
- PHP parallel lint scope:
ddev php-parallel-lintremains wrapper-scoped because the tool does not provide an equivalent project config file for default target paths.
- PHPStan default scope:
- The installed
phpstan.neonanalysesmodules,themes,profiles, andsitesunder the docroot, excludingcontribsubtrees andthemes/blank(the starter theme Drupal CMS site templates generate directly underthemes/). The broad parent directories are deliberate: projects that keep custom code outsidemodules/custom(for examplemodules/common/) are still analysed instead of being silently skipped. - Contrib is excluded with
analyseAndScan, so calls into contrib code still type-check (phpstan-drupal resolves contrib classes on demand via Drupal's autoloader) without scanning the contrib tree. - This intentionally deviates from the GitLab CI template
pathsvalues: the templates target contrib repos where the project root is the module under test, while this add-on targets site projects whose custom code is the target. Template parity applies to the rules, not the scan scope. - First adoption on an existing project may surface previously-unseen
findings. Run
ddev phpstan --generate-baselineonce to capture existing debt so only new regressions fail (see PHPStan baseline below).
- The installed
- PHPStan baseline:
- Generate a baseline with
ddev phpstan --generate-baseline. - This writes
phpstan-baseline.neonat the project root and updatesphpstan.neonto include it. - Use a baseline to suppress known issues in legacy code or core defaults
(for example, the shipped
settings.phpfiles), then work it down over time. Avoid using it to hide new regressions.
- Generate a baseline with
- PHPStan config requirement:
ddev phpstanrequires project config (phpstan.neon*) unless you pass--configuration <path>.
- PHPStan level:
- GitLab CI template defaults use level 0. The installer can set a local default level (0-10).
Installer environment variables
DCQ_INSTALL_MODE:replace,skip, orabortfor conflict handling.DCQ_NONINTERACTIVE=true: disable prompts; if no overrides are set, applies the recommended settings automatically.DCQ_PHPSTAN_LEVEL: setphpstan.neonlevel (0-10) without prompting.DCQ_ESLINT_QUIET:1/unset to suppress ESLint warnings by default (GitLab CI parity),0to include warnings. This can be set in.ddev/config.yamlunderweb_environment.DCQ_INSTALL_DEPS:install/trueto auto-install missingdrupal/core-dev,skip/falseto skip, or unset to prompt when interactive.DCQ_INSTALL_NODE_DEPS:rootto install JS deps in the project root (creates a rootpackage.jsonfrom core if missing; name uses the DDEV project name, and prompts to add missing Drupal deps when a rootpackage.jsonalready exists),install/trueto auto-install in the project root,skip/falseto skip, or unset to prompt (default: install in the project root). The installer selects npm/yarn based on existing lockfiles.DCQ_SCSS_SUPPORT:install/trueto configure Stylelint SCSS support when.scss/.sassfiles are detected (addsstylelint-config-standard-scssto the Node toolchain install, switches the.stylelintrc.jsonextendsentry to it when the shipped config can be updated automatically, and setsDCQ_STYLELINT_GLOBSto include**/*.scssin.ddev/config.drupal-code-quality.yaml),skip/falseto leave Stylelint CSS-only, or unset to prompt when interactive. Recommended-settings and non-interactive installs default toinstall, so SCSS projects get SCSS linting without declining the recommended path. If the config package cannot be installed (for example when Node tooling is skipped) or.stylelintrc.jsonhas a customextendslist, the install summary lists the remaining manual steps.DCQ_INSTALL_GITIGNORE:add/trueto adddcq-reports/to.gitignorewithout prompting,skip/falseto skip, or unset to prompt when interactive (default: yes).DCQ_INSTALL_IDE_SETTINGS:mergeto add missing VS Code settings and extension recommendations,overwriteto back up and replace,skipto handle manually, or unset to prompt.
Maintaining upstream configs
The add-on ships config files sourced from Drupal core and the Drupal GitLab CI templates. A sync script checks for upstream changes:
# Check for upstream config changes (report only)
./scripts/sync-upstream-configs.sh
# Apply changes to local asset files
./scripts/sync-upstream-configs.sh --update
# Override upstream branches
./scripts/sync-upstream-configs.sh --branch-core=11.x --branch-templates=main
After applying updates, run the full test suite to validate configs still work:
DCQ_FULL_TESTS=1 bats --jobs 4 ./tests/test.bats
A weekly GitHub Actions workflow (upstream-config-check.yml) automatically
checks for upstream drift and opens an issue when changes are detected.
Troubleshooting
Stylelint crashes with "RangeError: Invalid string length"
On projects with a very large number of Stylelint violations (2000+), the
default string formatter may crash because it builds a single table string
that exceeds Node.js memory limits. This is a
known stylelint limitation.
Workaround: Use the compact formatter, which outputs one line per warning
and does not hit the string length limit:
ddev stylelint --formatter compact
Stylelint crashes with "findLastIndex is not a function"
This means your DDEV container is running Node.js 16 or earlier.
Array.prototype.findLastIndex requires Node.js 18+. Update your DDEV config:
# .ddev/config.yaml
nodejs_version: "20"
Then run ddev restart.
Uninstall
Removing the add-on cleans up the .ddev payload (commands, shims, assets,
manifest, and dcq-install.sh); project-root configs remain in place
intentionally. Remove them manually if desired.
Credits
Contributed and maintained by @UltraBob