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