From 489160faaffe000912b37649e31e49288b235577 Mon Sep 17 00:00:00 2001 From: Mikkel Ricky Date: Fri, 19 Dec 2025 13:03:03 +0100 Subject: [PATCH] drupal-phpcs-sniffs --- .github/workflows/changelog.yaml | 29 +++++ .github/workflows/composer.yaml | 80 ++++++++++++ .github/workflows/markdown.yaml | 44 +++++++ .github/workflows/php.yaml | 59 +++++++++ .github/workflows/yaml.yaml | 41 +++++++ .gitignore | 2 + .markdownlint.jsonc | 22 ++++ .markdownlintignore | 12 ++ .phpcs.xml.dist | 31 +++++ CHANGELOG.md | 10 ++ .../Sniffs/Semantics/MethodLogSniff.php | 116 ++++++++++++++++++ ItkDevDrupal/ruleset.xml | 10 ++ README.md | 35 ++++++ composer.json | 20 ++- docker-compose.yml | 27 ++++ 15 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/changelog.yaml create mode 100644 .github/workflows/composer.yaml create mode 100644 .github/workflows/markdown.yaml create mode 100644 .github/workflows/php.yaml create mode 100644 .github/workflows/yaml.yaml create mode 100644 .gitignore create mode 100644 .markdownlint.jsonc create mode 100644 .markdownlintignore create mode 100644 .phpcs.xml.dist create mode 100644 CHANGELOG.md create mode 100644 ItkDevDrupal/Sniffs/Semantics/MethodLogSniff.php create mode 100644 ItkDevDrupal/ruleset.xml create mode 100644 docker-compose.yml diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..fead572 --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,29 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/changelog.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Changelog +### +### Checks that changelog has been updated + +name: Changelog + +on: + pull_request: + +jobs: + changelog: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 2 + + - name: Git fetch + run: git fetch + + - name: Check that changelog has been updated. + run: git diff --exit-code origin/${{ github.base_ref }} -- CHANGELOG.md && exit 1 || exit 0 diff --git a/.github/workflows/composer.yaml b/.github/workflows/composer.yaml new file mode 100644 index 0000000..a0e5a94 --- /dev/null +++ b/.github/workflows/composer.yaml @@ -0,0 +1,80 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/composer.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Composer +### +### Validates composer.json and checks that it's normalized. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [ergebnis/composer-normalize](https://github.com/ergebnis/composer-normalize) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev ergebnis/composer-normalize +### ``` +### +### Normalize `composer.json` by running +### +### ``` shell +### docker compose run --rm phpfpm composer normalize +### ``` + +name: Composer + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + composer-validate: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v5 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer validate --strict + + composer-normalized: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v5 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm composer normalize --dry-run + + composer-audit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v5 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer audit diff --git a/.github/workflows/markdown.yaml b/.github/workflows/markdown.yaml new file mode 100644 index 0000000..ae83163 --- /dev/null +++ b/.github/workflows/markdown.yaml @@ -0,0 +1,44 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/markdown.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Markdown +### +### Lints Markdown files (`**/*.md`) in the project. +### +### [markdownlint-cli configuration +### files](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration), +### `.markdownlint.jsonc` and `.markdownlintignore`, control what is actually +### linted and how. +### +### #### Assumptions +### +### 1. A docker compose service named `markdownlint` for running `markdownlint` +### (from +### [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli)) +### exists. + +name: Markdown + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + markdown-lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm markdownlint markdownlint '**/*.md' diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml new file mode 100644 index 0000000..7a092e9 --- /dev/null +++ b/.github/workflows/php.yaml @@ -0,0 +1,59 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/drupal-module/php.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Drupal module PHP +### +### Checks that PHP code adheres to the [Drupal coding +### standards](https://www.drupal.org/docs/develop/standards). +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [drupal/coder](https://www.drupal.org/project/coder) is a dev requirement +### in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev drupal/coder +### ``` +### +### Clean up and check code by running +### +### ``` shell +### docker compose run --rm phpfpm vendor/bin/phpcbf +### docker compose run --rm phpfpm vendor/bin/phpcs +### ``` +### +### > [!NOTE] +### > The template adds `.phpcs.xml.dist` as [a configuration file for +### > PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file) +### > and this makes it possible to override the actual configuration used in a +### > project by adding a more important configuration file, e.g. `.phpcs.xml`. + +name: PHP + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + coding-standards: + name: PHP - Check Coding Standards + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm vendor/bin/phpcs diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml new file mode 100644 index 0000000..631e525 --- /dev/null +++ b/.github/workflows/yaml.yaml @@ -0,0 +1,41 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/yaml.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### YAML +### +### Validates YAML files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. +### +### #### Symfony YAML +### +### Symfony's YAML config files use 4 spaces for indentation and single quotes. +### Therefore we use a [Prettier configuration +### file](https://prettier.io/docs/configuration), `.prettierrc.yaml`, to make +### Prettier format YAML files in the `config/` folder like Symfony expects. + +name: YAML + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + yaml-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier '**/*.{yml,yaml}' --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7579f74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +composer.lock diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..0253096 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,22 @@ +// This file is copied from config/markdown/.markdownlint.jsonc in https://github.com/itk-dev/devops_itkdev-docker. +// Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +// markdownlint-cli configuration file (cf. https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration) +{ + "default": true, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md + "line-length": { + "line_length": 120, + "code_blocks": false, + "tables": false + }, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md + "no-duplicate-heading": { + "siblings_only": true + }, + // https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections#creating-a-collapsed-section + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md + "no-inline-html": { + "allowed_elements": ["details", "summary"] + } +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..d143ace --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,12 @@ +# This file is copied from config/markdown/.markdownlintignore in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#ignoring-files +vendor/ +node_modules/ +LICENSE.md +# Drupal +web/*.md +web/core/ +web/libraries/ +web/*/contrib/ diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..a97dd83 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,31 @@ + + + + + + The coding standard. + + . + + + node_modules + vendor + *.css + *.js + + + + + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..87415f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/itk-dev/drupal-phpcs-sniffs diff --git a/ItkDevDrupal/Sniffs/Semantics/MethodLogSniff.php b/ItkDevDrupal/Sniffs/Semantics/MethodLogSniff.php new file mode 100644 index 0000000..4049bca --- /dev/null +++ b/ItkDevDrupal/Sniffs/Semantics/MethodLogSniff.php @@ -0,0 +1,116 @@ +getTokens(); + $name = $tokens[$stackPtr]['content'] ?? NULL; + $argument = $this->getArgument(1); + + // Lifted from parent::processFunctionCall with `t()` replaced with + // `$name()` and some warnings raised to errors. + if ($argument === FALSE) { + $error = "Empty calls to $name() are not allowed"; + $phpcsFile->addError($error, $stackPtr, 'EmptyLog'); + return; + } + + if ($tokens[$argument['start']]['code'] !== T_CONSTANT_ENCAPSED_STRING) { + // Not a translatable string literal. + $warning = "Only string literals should be passed to $name()"; + $phpcsFile->addError($warning, $argument['start'], 'NotLiteralString'); + return; + } + + $string = $tokens[$argument['start']]['content']; + if ($string === '""' || $string === "''") { + $warning = "Do not pass empty strings to $name()"; + $phpcsFile->addError($warning, $argument['start'], 'EmptyString'); + return; + } + + $concatAfter = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeBracket + 1), NULL, TRUE, NULL, TRUE); + if ($concatAfter !== FALSE && $tokens[$concatAfter]['code'] === T_STRING_CONCAT) { + $stringAfter = $phpcsFile->findNext(Tokens::$emptyTokens, ($concatAfter + 1), NULL, TRUE, NULL, TRUE); + if ($stringAfter !== FALSE + && $tokens[$stringAfter]['code'] === T_CONSTANT_ENCAPSED_STRING + && $this->checkConcatString($tokens[$stringAfter]['content']) === FALSE + ) { + $warning = "Do not concatenate strings to translatable strings, they should be part of the $name() argument and you should use placeholders"; + $phpcsFile->addWarning($warning, $stringAfter, 'ConcatString'); + } + } + + $lastChar = substr($string, -1); + if ($lastChar === '"' || $lastChar === "'") { + $message = substr($string, 1, -1); + if ($message !== trim($message)) { + $warning = "Translatable strings must not begin or end with white spaces, use placeholders with $name() for variables"; + $phpcsFile->addWarning($warning, $argument['start'], 'WhiteSpace'); + } + } + + $concatFound = $phpcsFile->findNext(T_STRING_CONCAT, $argument['start'], $argument['end']); + if ($concatFound !== FALSE) { + $error = 'Concatenating translatable strings is not allowed, use placeholders instead and only one string literal'; + $phpcsFile->addError($error, $concatFound, 'Concat'); + } + + // Check if there is a backslash escaped single quote in the string and + // if the string makes use of double quotes. + if ($string[0] === "'" && strpos($string, "\'") !== FALSE + && strpos($string, '"') === FALSE + ) { + $warn = 'Avoid backslash escaping in translatable strings when possible, use "" quotes instead'; + $phpcsFile->addWarning($warn, $argument['start'], 'BackslashSingleQuote'); + return; + } + + if ($string[0] === '"' && strpos($string, '\"') !== FALSE + && strpos($string, "'") === FALSE + ) { + $warn = "Avoid backslash escaping in translatable strings when possible, use '' quotes instead"; + $phpcsFile->addWarning($warn, $argument['start'], 'BackslashDoubleQuote'); + } + + } + +} diff --git a/ItkDevDrupal/ruleset.xml b/ItkDevDrupal/ruleset.xml new file mode 100644 index 0000000..7b88a25 --- /dev/null +++ b/ItkDevDrupal/ruleset.xml @@ -0,0 +1,10 @@ + + + Additional Drupal conding standards + + + diff --git a/README.md b/README.md index 1d6f1b4..9c5ceee 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ # ITK Dev Drupal coding standards + +Additional Drupal coding standards sniffs. + +## Sniffs + +| Sniff | Description | +|------------------------------------|------------------------------------------------------------------------| +| `ItkDevDrupal.Semantics.MethodLog` | Check that first argument to the `log` method[^1] is a constant string | + +[^1]: And its convenient helper friends from + [LoggerTrait](https://github.com/php-fig/log/blob/master/src/LoggerTrait.php) as well. + +## Installation + +``` shell +composer require --dev itk-dev/drupal-phpcs-sniffs +``` + +## Use + +Include the `ItkDevDrupal` rule in your ruleset, e.g. + +``` xml + + + … + + +``` + +or use the `ItkDevDrupal` standard specifically: + +``` shell +php vendor/bin/phpcs --standard=ItkDevDrupal +``` diff --git a/composer.json b/composer.json index d32d770..60366f0 100644 --- a/composer.json +++ b/composer.json @@ -12,5 +12,23 @@ "name": "Mikkel Ricky", "email": "rimi@aarhus.dk" } - ] + ], + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "drupal/coder": "^8.3" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.48" + }, + "autoload": { + "psr-4": { + "ItkDevDrupal\\": "ItkDevDrupal/" + } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "ergebnis/composer-normalize": true + } + } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0b9f650 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +# itk-version: 3.2.4 + +services: + phpfpm: + image: itkdev/php8.4-fpm:latest + user: ${COMPOSE_USER:-deploy} + volumes: + - .:/app + + # Code checks tools + markdownlint: + image: itkdev/markdownlint + profiles: + - dev + volumes: + - ./:/md + + prettier: + # Prettier does not (yet, fcf. + # https://github.com/prettier/prettier/issues/15206) have an official + # docker image. + # https://hub.docker.com/r/jauderho/prettier is good candidate (cf. https://hub.docker.com/search?q=prettier&sort=updated_at&order=desc) + image: jauderho/prettier + profiles: + - dev + volumes: + - ./:/work