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.
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 ./publicPHP_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.
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 ./srcruleset.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 | 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 |
git clone https://github.com/BemidjiState/bsuwordpresscs
cd bsuwordpresscs
nvm use
npm install
composer installnpm 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.
# 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 testBSUWordPressCS uses PHP_CodeSniffer's built-in sniff testing framework, which is different from vanilla PHPUnit. Understanding it is essential before writing tests.
Rather than writing individual test*() methods with assertEquals() calls, you:
- Write a fixture file (
.inc) containing real PHP code — both passing and failing examples. - Declare expected errors by line number in a
getErrorList()method on the test class. - The inherited
testSniff()method (fromAbstractSniffTestCase) 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.
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
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.
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' );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.
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.
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.
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.
npm run test # all tests must pass
npm run lint # the sniff file itself must pass BSUWordPressCSRun linting and tests manually to verify before committing. The pre-commit hook also enforces this automatically:
npm run lint
npm run testThis 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
Releases follow the same semver + GitHub Actions pattern used in the BSUwp theme.
feature/my-change → PR → release → PR → main
- Feature branches — all work starts here; PRs target the
releasebranch. releasebranch — staging area for the next stable release. Direct pushes blocked locally by the pre-push hook.main— stable. Mergingrelease→maintriggers the release workflow. Direct pushes blocked locally by the pre-push hook.
GitHub Actions handles all versioning and tagging when a PR is merged to main — there is no manual release step.
- The workflow inspects conventional commit messages since the last tag.
- It calculates the next semver bump (
feat→ minor,fix→ patch, breaking change → major). - It updates
package.jsonwith the new version and commits it. - 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.
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.