Skip to content

BemidjiState/bsuwordpresscs

Repository files navigation

BSUWordPressCS

Custom PHP_CodeSniffer sniffs for BSU/NTC WordPress coding standards. Used by the BSUwp theme to enforce BSU-specific PHP coding conventions on top of the WordPress Coding Standards.

Requirements

How This Package Is Used in the Theme

This package is installed as a private npm dependency in the BSUwp theme:

"@bsu/BSUWordPressCS": "github:BemidjiState/bsuwordpresscs"

The theme's phpcs lint command references the BSUWordPressCS standard by name:

phpcs --standard=BSUWordPressCS ./public

PHP_CodeSniffer locates the standard via the installed_paths config, which the theme sets to point at node_modules/@bsu/BSUWordPressCS. The published npm package ships only Sniffs/, ruleset.xml, README.md, and package.json — all dev tooling is excluded via .npmignore.

Installing via Composer

Because composer.json declares "type": "phpcodesniffer-standard", this package can also be required as a Composer dependency. The dealerdirect/phpcodesniffer-composer-installer plugin auto-registers the standard with PHP_CodeSniffer at install time.

Add a VCS repository entry pointing at your private copy and then require the package:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://bitbucket.bemidjistate.edu/scm/npm/bsuwordpresscs.git"
        }
    ],
    "require": {
        "bsu/bsuwordpresscs": "*"
    },
    "config": {
        "allow-plugins": {
            "dealerdirect/phpcodesniffer-composer-installer": true
        }
    }
}

After composer install, the standard is available by name:

phpcs --standard=BSUWordPressCS ./src

Excluded WordPress Rules

ruleset.xml extends the full WordPress standard and excludes the following upstream rules to align with BSU conventions:

Excluded Rule Reason
PSR2.Methods.FunctionClosingBrace Conflicts with BSU brace spacing requirements
Squiz.Commenting.BlockComment.NoNewLine BSU uses single-line block comments in specific contexts
Squiz.Commenting.BlockComment.HasEmptyLineBefore BSU does not require blank lines before block comments
Squiz.Commenting.InlineComment.InvalidEndChar Covered by InlineCommentSniff
Squiz.Commenting.InlineComment.SpacingAfterAtFunctionEnd Not applicable in BSU style
Squiz.Commenting.VariableComment.TodoTagNotAllowed TODO tags are permitted
WordPress.WhiteSpace.ControlStructureSpacing.BlankLineAfterEnd Conflicts with NoBlankLineBeforeCloseBraceSniff

Sniff Reference

Sniff Category What It Enforces Fixable
NoBlankLineAfterOpenBraceSniff Spacing Requires a blank line after the opening brace of functions, closures, and control structures. Exempt when the block opens and closes immediately (consecutive lines). Functions with return type declarations are handled correctly via scope_opener. Yes
NoBlankLineBeforeCloseBraceSniff Spacing Requires a blank line before the closing brace of all closures and control structures. Exempt for consecutive closing braces and dynamic property assignments. Yes
FunctionDocBlockSpacingSniff Spacing Requires exactly 7 blank lines before a function docblock in general scope. Requires exactly 1 blank line when the function is the first item inside a scope (class, if, try, etc.). Yes
GetSiteUrlSniff Functions Flags get_site_url() usage and directs developers to use $bsu_theme_settings->current_blog_details->siteurl instead. No
GetTemplateDirectorySniff Functions Flags get_template_directory() usage and directs developers to use $bsu_theme_settings->theme_directory instead. No
InlineCommentSniff Commenting Flags // comments appearing after code on the same line and auto-fixes them to /* ... */ format. phpcs directives are exempt. Yes
SingleLineCommentSniff Commenting Flags single-line // and /* ... */ comments on their own line and directs developers to use the multi-line PHPDoc format instead. No

Development Workflow

Setup

git clone https://github.com/BemidjiState/bsuwordpresscs
cd bsuwordpresscs
nvm use
npm install
composer install

npm install installs Husky and commit tooling. composer install installs PHP_CodeSniffer, WPCS, and PHPUnit into vendor/, and the dealerdirect/phpcodesniffer-composer-installer plugin automatically registers BSUWordPressCS in vendor/squizlabs/php_codesniffer/CodeSniffer.conf so that phpcs -i lists it.

Daily Development

# Lint the sniff PHP files against BSUWordPressCS itself.
npm run lint

# Auto-fix lint issues.
npm run lint:fix

# Run all PHPUnit sniff tests.
npm run test

Test Infrastructure

BSUWordPressCS uses PHP_CodeSniffer's built-in sniff testing framework, which is different from vanilla PHPUnit. Understanding it is essential before writing tests.

How it works

Rather than writing individual test*() methods with assertEquals() calls, you:

  1. Write a fixture file (.inc) containing real PHP code — both passing and failing examples.
  2. Declare expected errors by line number in a getErrorList() method on the test class.
  3. The inherited testSniff() method (from AbstractSniffTestCase) runs the sniff against the fixture file, collects the actual errors found, and compares them to your declarations. Any mismatch is a test failure.

For fixable sniffs, a second expected-output file (.inc.fixed) holds what the file should look like after phpcbf runs. The framework runs the fixer in memory and diffs the result against your .inc.fixed file.

File layout

Tests/
├── AbstractBSUSniffTestCase.php       ← base class, extends AbstractSniffTestCase
├── bootstrap.php                      ← PHPUnit bootstrap: constants, autoloaders, installed paths
├── Commenting/
│   ├── InlineCommentUnitTest.php      ← test class (getErrorList / getWarningList)
│   ├── InlineCommentUnitTest.inc      ← fixture: PHP code with passing and failing examples
│   └── InlineCommentUnitTest.inc.fixed← expected output after phpcbf (fixable sniffs only)
├── Functions/
│   ├── GetSiteUrlUnitTest.php
│   └── GetSiteUrlUnitTest.inc
└── Spacing/
    ├── FunctionDocBlockSpacingUnitTest.php
    ├── FunctionDocBlockSpacingUnitTest.inc
    └── FunctionDocBlockSpacingUnitTest.inc.fixed

AbstractBSUSniffTestCase

All test classes extend AbstractBSUSniffTestCase (not AbstractSniffTestCase directly). The base class overrides setUp() to reset PHP_CodeSniffer's static config cache before each test. This is necessary because ConfigDouble (used by the PHPCS test framework) wipes the static configData property, which removes the installed_paths that dealerdirect/phpcodesniffer-composer-installer wrote to CodeSniffer.conf. The reset forces getAllConfigData() to re-read the conf file so BSUWordPressCS sniffs are found.


Adding a New Sniff

1. Create the sniff file

Create Sniffs/<Category>/<Name>Sniff.php. Use an existing sniff as a template.

Sniffs/
├── Commenting/
├── Functions/
└── Spacing/        ← put spacing-related sniffs here, etc.

Namespace and class:

namespace BSUWordPressCS\Sniffs\<Category>;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;

class <Name>Sniff implements Sniff {

    public function register() {
        return array( T_FUNCTION ); // whichever token(s) to listen for
    }

    public function process( File $phpcs_file, $stack_position ) {
        // sniff logic here
    }

}

Error codes — critical: Pass a short, plain string as the third argument to addError() or addFixableError() — never a dotted BSUWordPressCS.Category.Name code. PHPCS constructs the full code automatically from the sniff class name. Passing a dotted string is treated as a pre-composed full code, which breaks the sniff-filter check and silently drops all errors.

// Correct
$phpcs_file->addError( $message, $stack_position, 'MyErrorCode' );

// Wrong — errors will be silently dropped
$phpcs_file->addError( $message, $stack_position, 'BSUWordPressCS.Spacing.MySniff' );

2. Register the sniff in ruleset.xml

The three existing category directories are already included:

<rule ref="./Sniffs/Commenting"/>
<rule ref="./Sniffs/Functions"/>
<rule ref="./Sniffs/Spacing"/>

If you add a sniff to one of those directories, no change to ruleset.xml is needed. If you add a new category directory, add a corresponding <rule ref="./Sniffs/NewCategory"/> line.

3. Create the test class

Create Tests/<Category>/<Name>UnitTest.php:

namespace BSUWordPressCS\Tests\<Category>;

use BSUWordPressCS\Tests\AbstractBSUSniffTestCase;

class <Name>UnitTest extends AbstractBSUSniffTestCase {

    public function getErrorList( $testFile = '' ) {
        return array(
            10 => 1,  // line 10 should have exactly 1 error
            25 => 2,  // line 25 should have exactly 2 errors
        );
    }

    public function getWarningList( $testFile = '' ) {
        return array(); // return line => count pairs for warnings, or empty
    }

}

getErrorList() and getWarningList() return arrays mapping line number → expected count. Any line not listed is expected to have zero errors/warnings.

4. Create the fixture file

Create Tests/<Category>/<Name>UnitTest.inc with real PHP code that exercises both passing and failing cases.

<?php
/**
 * Test fixture for <Name>Sniff.
 */

// passing example — no error expected on this line
$valid = something_allowed();

$invalid = something_flagged(); // error expected on this line (matches getErrorList)

Important: Do not use standalone // comments as labels inside fixture files — SingleLineCommentSniff flags them too. Use /** docblock comments, /* multi-line */ blocks, or blank lines to separate sections. Reserve // phpcs:ignore only when you need to suppress a rule on a specific line.

If your sniff produces errors on multiple lines, list each line in getErrorList(). The fixture does not need any special annotation — the line numbers in getErrorList() are the single source of truth.

5. Add a fixed file (fixable sniffs only)

If your sniff uses addFixableError() and implements fixer logic in the $fix === true branch, create Tests/<Category>/<Name>UnitTest.inc.fixed.

The file must contain exactly what phpcbf would output when run on the .inc fixture — every error fixed, all other content unchanged. The name is <fixture-filename>.fixed, meaning the full extension is .inc.fixed (appended to the .inc filename, not replacing it).

InlineCommentUnitTest.inc        ← input with violations
InlineCommentUnitTest.inc.fixed  ← expected output after phpcbf

The framework runs the fixer in memory after processing the .inc file and diffs fixer->getContents() against your .inc.fixed. If they differ, the test fails with a diff.

6. Verify

npm run test    # all tests must pass
npm run lint    # the sniff file itself must pass BSUWordPressCS

Before Committing

Run linting and tests manually to verify before committing. The pre-commit hook also enforces this automatically:

npm run lint
npm run test

Commit Message Convention

This project uses Conventional Commits enforced by commitlint and Husky.

Format:

<type>(<scope>): <short description>

Allowed types:

Type When to use
feat New sniff or new sniff behaviour
fix Bug fix in an existing sniff
build Build system or dependency changes
chore Maintenance tasks (no production code change)
ci CI/CD configuration changes
docs Documentation only
perf Performance improvement
refactor Code restructure without behaviour change
revert Reverting a previous commit
style Formatting, whitespace (no logic change)
test Adding or updating tests

Allowed scopes:

Scope When to use
ci GitHub Actions workflow changes
config Changes to project config files
deps Composer or npm dependency updates
docs README changes
release Version bumps (used by the GitHub Actions release bot)
ruleset Changes to ruleset.xml
sniffs Changes to any sniff PHP file
tests Changes to test classes or fixture files

Examples:

feat(sniffs): add RequireGlobalSniff to flag direct global usage
fix(sniffs): correct FunctionDocBlockSpacingSniff line count off-by-one
test(tests): add fixture cases for dynamic property assignment exemption
docs(docs): add sniff reference table to README
ci(ci): add pull request validation workflow

Branch and Release Strategy

Releases follow the same semver + GitHub Actions pattern used in the BSUwp theme.

Branch flow

feature/my-change  →  PR  →  release  →  PR  →  main
  • Feature branches — all work starts here; PRs target the release branch.
  • release branch — staging area for the next stable release. Direct pushes blocked locally by the pre-push hook.
  • main — stable. Merging releasemain triggers the release workflow. Direct pushes blocked locally by the pre-push hook.

Automated releases

GitHub Actions handles all versioning and tagging when a PR is merged to main — there is no manual release step.

  1. The workflow inspects conventional commit messages since the last tag.
  2. It calculates the next semver bump (feat → minor, fix → patch, breaking change → major).
  3. It updates package.json with the new version and commits it.
  4. It creates a git tag (X.Y.Z) and pushes it.

No prerelease tags or GitHub release pages are created — the git tag is the release artifact.

Updating the BSUwp theme

After a release, update the version reference in the BSUwp theme's package.json:

"@bsu/BSUWordPressCS": "git+https://github.com/BemidjiState/bsuwordpresscs.git#X.Y.Z"

Then run npm install in the theme repo.

About

Custom PHPCodeSniffer rules for WordPress development.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors