Linting and Code Compliance
A linter is a tool that reads your code and flags problems: syntax errors, style inconsistencies, patterns that are likely bugs, rules you’ve decided the project should follow. It is automated code review that runs in milliseconds.
There are three linters to set up. ESLint covers JavaScript. Stylelint covers CSS and Sass. PHP_CodeSniffer with the WordPress Coding Standards ruleset covers PHP. You won’t write any PHP until Chapter 12, but setting up PHPCS now means it’s already wired into the pipeline when you need it.
The goal is to write compliant code from the first line, not to go back and fix it later. Refactoring non-compliant code at scale is not fun. Ask anyone who’s inherited a legacy codebase.
ESLint
Install
npm install --save-dev eslint @eslint/jsConfigure
ESLint 9 uses a flat config file: eslint.config.js in the project root. Create it:
touch eslint.config.jsAdd this configuration:
const js = require('@eslint/js');
module.exports = [
js.configs.recommended,
{
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'no-undef': 'error',
'eqeqeq': ['error', 'always'],
'curly': 'error',
},
languageOptions: {
globals: {
document: 'readonly',
window: 'readonly',
console: 'readonly',
},
},
},
];js.configs.recommended enables ESLint’s built-in recommended rules. The entries in rules extend or override them. 'warn' flags a problem without failing the lint check. 'error' fails it. eqeqeq enforces === instead of ==, which avoids JavaScript’s type coercion surprises. curly requires braces on all control flow statements, even single-line ones.
The globals block tells ESLint that document, window, and console are globals provided by the browser environment, so it won’t flag them as undefined variables.
Test it
Add a deliberate error to src/js/toggle.js: an unused variable.
const unused = 'this should fail';
export default function setupToggle(buttonSelector, targetSelector) {
// ...
}Run ESLint manually:
npx eslint src/js/It should flag unused as an error. Remove it, run again, and get a clean result.
Add a Gulp task
Install the current maintained Gulp ESLint plugin:
npm install --save-dev gulp-eslint-newAdd the task to gulpfile.js:
const eslint = require('gulp-eslint-new');
function lintJs() {
return src('src/js/**/*.js')
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
}
exports.lintJs = lintJs;failAfterError() stops the Gulp task if ESLint finds errors, but lets it finish reporting all problems first. Wire it into your build:
exports.build = series(lintJs, parallel(html, css, js));This runs the lint check before building. A failing lint check stops the build.
Stylelint
Install
npm install --save-dev stylelint stylelint-config-standard-scssConfigure
Create .stylelintrc.json in the project root:
{
"extends": ["stylelint-config-standard-scss"],
"rules": {
"color-named": "never",
"declaration-no-important": true,
"selector-max-id": 0,
"max-nesting-depth": 3
}
}color-named bans color names like red or blue in favor of explicit hex or variable values. declaration-no-important bans !important, which is almost always a sign of a specificity problem that should be solved properly. selector-max-id bans ID selectors in your Sass entirely, enforcing the class-based approach from Chapter 5. max-nesting-depth enforces the three-level nesting limit covered in Chapter 9.
Test it
Add a named color to _base.scss:
h1 {
color: red; // this should fail
}Run Stylelint manually:
npx stylelint "src/scss/**/*.scss"It flags the named color. Remove it, run again, clean result.
Add a Gulp task
The most reliable approach is to shell out directly using Node’s child_process:
const { exec } = require('child_process');
function lintCss(done) {
exec('npx stylelint "src/scss/**/*.scss"', (error, stdout, stderr) => {
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
done(error);
});
}
exports.lintCss = lintCss;Wire it into the build alongside lintJs:
exports.build = series(parallel(lintJs, lintCss), parallel(html, css, js));PHP_CodeSniffer and WordPress Coding Standards
PHPCS is a PHP tool, not a Node tool. It requires PHP and Composer, PHP’s package manager. You don’t have any PHP yet, but setting this up now means the linter is ready when the WordPress chapters start.
Install PHP
macOS:
brew install phpVerify:
php --versionUbuntu:
sudo apt update
sudo apt install php php-cli php-xmlInstall Composer
macOS:
brew install composerUbuntu:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
sudo mv composer.phar /usr/local/bin/composerVerify:
composer --versionInstall PHPCS and WordPress Coding Standards
composer global require squizlabs/php_codesniffer
composer global require wp-coding-standards/wpcs:"*"Register the WordPress ruleset with PHPCS:
phpcs --config-set installed_paths $(composer global config home)/vendor/wp-coding-standards/wpcsVerify the WordPress ruleset is available:
phpcs -iYou should see WordPress, WordPress-Core, WordPress-Docs, and WordPress-Extra in the list.
Make sure your global Composer bin directory is on your PATH. Add this to your shell configuration file (.zshrc on macOS, .bashrc on Ubuntu, or your Fish config if you use Fish):
export PATH="$PATH:$HOME/.composer/vendor/bin"Restart your terminal or source the config file, then verify:
phpcs --versionCreate a PHPCS configuration file
Create phpcs.xml in the project root:
<?xml version="1.0"?>
<ruleset name="WordPress Theme">
<description>WordPress Coding Standards for this theme.</description>
<rule ref="WordPress" />
<arg name="extensions" value="php" />
<arg name="colors" />
<arg value="sp" />
<file>./src/php</file>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
</ruleset>Create the src/php/ directory:
mkdir src/phpAdd a Gulp task
function lintPhp(done) {
exec('phpcs', (error, stdout, stderr) => {
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
// PHPCS exits with code 1 for warnings, 2 for errors
// Only fail the task on errors, not warnings
done(error && error.code === 2 ? error : null);
});
}
exports.lintPhp = lintPhp;Add it to the build:
exports.build = series(parallel(lintJs, lintCss, lintPhp), parallel(html, css, js));With an empty src/php/, PHPCS finds nothing to check and exits cleanly.
VS Code extensions
Lint checks in the terminal are good. Lint checks as you type are better. Install these extensions so VS Code underlines problems before you even save:
- ESLint: installed in Chapter 3, now picks up
eslint.config.jsautomatically - Stylelint: search and install from the Extensions panel
- PHP Sniffer & Beautifier: runs PHPCS against PHP files in the editor
Add Stylelint validation for .scss files to your VS Code settings.json:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"stylelint.validate": ["css", "scss"]
}The full updated Gulpfile
Here’s gulpfile.js in its complete state after this chapter:
const { src, dest, watch, series, parallel } = require('gulp');
const browserSync = require('browser-sync').create();
const sass = require('gulp-sass')(require('sass'));
const rollup = require('rollup');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const eslint = require('gulp-eslint-new');
const { exec } = require('child_process');
// HTML
function html() {
return src('src/**/*.html')
.pipe(dest('dist/'));
}
// CSS: compile Sass
function css() {
return src('src/scss/main.scss')
.pipe(sass({ outputStyle: 'expanded' }).on('error', sass.logError))
.pipe(dest('dist/css/'));
}
// JS: bundle with Rollup
async function js() {
const bundle = await rollup.rollup({
input: 'src/js/main.js',
plugins: [nodeResolve()],
});
await bundle.write({
file: 'dist/js/main.js',
format: 'iife',
name: 'App',
sourcemap: true,
});
}
// Lint: JavaScript
function lintJs() {
return src('src/js/**/*.js')
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
}
// Lint: CSS/Sass
function lintCss(done) {
exec('npx stylelint "src/scss/**/*.scss"', (error, stdout, stderr) => {
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
done(error);
});
}
// Lint: PHP
function lintPhp(done) {
exec('phpcs', (error, stdout, stderr) => {
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
done(error && error.code === 2 ? error : null);
});
}
// Dev server and file watching
function serve() {
browserSync.init({
server: {
baseDir: './dist',
},
});
watch('src/**/*.html', series(html, reload));
watch('src/scss/**/*.scss', series(css, reload));
watch('src/js/**/*.js', series(js, reload));
}
function reload(done) {
browserSync.reload();
done();
}
// Exports
exports.lintJs = lintJs;
exports.lintCss = lintCss;
exports.lintPhp = lintPhp;
exports.lint = parallel(lintJs, lintCss, lintPhp);
exports.build = series(parallel(lintJs, lintCss, lintPhp), parallel(html, css, js));
exports.default = series(parallel(html, css, js), serve);Note that the default (development) task doesn’t run linting before serving. Running lint checks on every file save during development is slow and disruptive. VS Code handles that feedback loop in real time. The build task is where linting gates the output. Before you ship anything, every check has to pass.
Update package.json to expose a lint script:
"scripts": {
"start": "gulp",
"build": "gulp build",
"lint": "gulp lint"
}Commit your work
git add .
git commit -m "Add ESLint, Stylelint, and PHPCS with WordPress Coding Standards"What you now know
You have automated code quality checks for JavaScript, Sass, and PHP, wired into both your editor and your build pipeline. Code that doesn’t meet the WordPress Coding Standards won’t make it into a build. In Chapter 12 you’ll use all of this: setting up a local WordPress environment and writing your first PHP.
Reference