Skip to content

Add three new sniffs for LearnDash coding standards#8

Open
nikolaystrikhar wants to merge 6 commits intomainfrom
feature/add-new-sniffs
Open

Add three new sniffs for LearnDash coding standards#8
nikolaystrikhar wants to merge 6 commits intomainfrom
feature/add-new-sniffs

Conversation

@nikolaystrikhar
Copy link

@nikolaystrikhar nikolaystrikhar commented Jan 29, 2026

Summary

This PR adds two new sniffs to enforce LearnDash coding standards:

  1. DisallowNonPrivateConstSniff - Prevents public/protected class constants
  2. MultilineConditionFormattingSniff - Enforces proper multiline condition formatting

All sniffs are auto-fixable with phpcbf.


1. LearnDash.Classes.DisallowNonPrivateConst

Purpose

Public and protected class constants cannot be deprecated, so we should use static properties instead to allow for future deprecation.

What it catches

// ❌ ERROR: Public class constant
class Example {
    public const MY_CONST = 'value';
}

// ❌ ERROR: Protected class constant
class Example {
    protected const MY_CONST = 'value';
}

// ❌ ERROR: Implicit public (no visibility keyword)
class Example {
    const MY_CONST = 'value';
}

// ❌ ERROR: Interface constants (always implicitly public)
interface Example {
    const MY_CONST = 'value';
}

What passes

// ✅ OK: Private class constant
class Example {
    private const MY_CONST = 'value';
}

// ✅ OK: Global constants (not in a class)
const GLOBAL_CONST = 'value';

Error codes

  • PublicConst - Explicit public constant
  • ProtectedConst - Protected constant
  • ImplicitPublicConst - Constant without visibility modifier

2. LearnDash.CodeAnalysis.MultilineConditionFormatting

Purpose

When conditions have multiple boolean expressions, they should be formatted for readability:

  • Each condition on its own line
  • Boolean operators (&&, ||) at the beginning of lines, not the end
  • Remove unnecessary parentheses around single conditions
  • Applies recursively to nested sub-conditions

What it catches

// ❌ ERROR: Multiple conditions on single line
if ( $a && $b || $c ) {
    // ...
}

// ❌ ERROR: Operator at END of line
if (
    $a &&
    $b
) {
    // ...
}

// ❌ ERROR: Unnecessary parentheses around single condition
if (
    ( ! empty( $a ) )
    && ( ! empty( $b ) )
) {
    // ...
}

// ❌ ERROR: Nested with operators at end
if (
    $a &&
    (
        $b ||
        $c
    )
) {
    // ...
}

What passes (after auto-fix)

// ✅ OK: Single condition on one line
if ( $a ) {
    // ...
}

// ✅ OK: Multiline with operators at start
if (
    $a
    && $b
    || $c
) {
    // ...
}

// ✅ OK: No unnecessary parentheses
if (
    ! empty( $a )
    && ! empty( $b )
) {
    // ...
}

// ✅ OK: Nested conditions properly formatted
if (
    $a
    && (
        $b
        || $c
    )
) {
    // ...
}

// ✅ OK: Four levels deep
if (
    $a
    && (
        $b
        || (
            $c
            && (
                $d
                || $e
            )
        )
    )
) {
    // ...
}

Error codes

  • SingleLineMultipleConditions - Multiple boolean expressions on a single line
  • OperatorNotAtLineStart - Boolean operator appears after other code on the same line
  • UnnecessaryParentheses - Parentheses around a single condition with no boolean operators

Supported control structures

  • if
  • elseif
  • while
  • for
  • switch

Usage

Add to your phpcs.xml:

<rule ref="LearnDash.Classes.DisallowNonPrivateConst"/>
<rule ref="LearnDash.CodeAnalysis.MultilineConditionFormatting"/>

Or run individually:

phpcs --standard=LearnDash --sniffs=LearnDash.Classes.DisallowNonPrivateConst src/
phpcs --standard=LearnDash --sniffs=LearnDash.CodeAnalysis.MultilineConditionFormatting src/

Test cases verified

MultilineConditionFormattingSniff

Before After Issue Fixed
if ( $a && $b || $c ) Split to multiline Single line multi-condition
$a &&
$b
$a
&& $b
Operator at end of line
( ! empty( $a ) ) ! empty( $a ) Unnecessary parentheses
Nested ( $b || $c ) with && at end Properly indented, operators at start Nested formatting
while ( $a && $b ) Split to multiline While loop support
elseif ( $a && $b ) Split to multiline Elseif support
4 levels deep nesting Each level properly indented with tabs Deep nesting support

Test plan

  • DisallowNonPrivateConstSniff catches public, protected, and implicit public constants
  • DisallowNonPrivateConstSniff allows private constants and global constants
  • MultilineConditionFormattingSniff catches single-line multi-conditions
  • MultilineConditionFormattingSniff catches operators at end of lines
  • MultilineConditionFormattingSniff catches unnecessary parentheses
  • MultilineConditionFormattingSniff works recursively on nested sub-conditions (up to 4+ levels)
  • MultilineConditionFormattingSniff works with while, elseif, for, switch
  • MultilineConditionFormattingSniff auto-fix produces consistent tab indentation (no mixed spaces/tabs)
  • All sniffs properly register with PHPCS

🤖 Generated with Claude Code

nikolaystrikhar and others added 3 commits January 29, 2026 12:10
- DisallowNonPrivateConstSniff: Prevents public/protected class constants
- DisallowHardcodedPostTypeSlugsSniff: Catches hardcoded post type slugs
- MultilineConditionFormattingSniff: Enforces proper multiline condition formatting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The sniff can now automatically fix:
- Single-line conditions with multiple expressions → split to multiline
- Operators at end of line → move to start of next line
- Nested conditions with proper indentation alignment

Run with: phpcbf --sniffs=LearnDash.CodeAnalysis.MultilineConditionFormatting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use a simple public $indent property (default: tab) instead of
auto-detection. This follows the WordPress sniff pattern and can
be configured in phpcs.xml if needed:

<rule ref="LearnDash.CodeAnalysis.MultilineConditionFormatting">
    <properties>
        <property name="indent" value="    "/>
    </properties>
</rule>

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comment on lines 65 to 66
// Get the string content without quotes.
$content = trim( $token['content'], '"\'' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is too loose and results in false positives on text like

esc_html( learndash_get_custom_label_lower( 'groups' ) ),

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a huge problem with it though, how do we decide which context is right and wrong and maintain it easily? Imagine I skip one case and then it becomes problematic to update again, we add more functions... I don't like the maintainability aspect of this sniff. Maybe it was a bad idea and we just delete it. Thoughts @estevao90 @d4mation ?

Comment on lines 199 to 203
$fix = $phpcs_file->addFixableWarning(
'Conditions with multiple boolean expressions should be split across multiple lines, with one condition per line.',
$open_paren,
'SingleLineMultipleConditions'
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warnings don't cause CI to fail, so maybe we should use error instead? The other existing sniffs all use error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair!

Comment on lines +246 to +258
/**
* Fixes a single-line condition by splitting it across multiple lines.
*
* @param File $phpcs_file The file being scanned.
* @param int[] $operators Array of operator token positions.
* @param int $open_paren The opening parenthesis position.
* @param int $close_paren The closing parenthesis position.
* @param string $base_indent The base indentation.
* @param int $depth The nesting depth.
*
* @return void
*/
private function fix_single_line_condition( File $phpcs_file, array $operators, int $open_paren, int $close_paren, string $base_indent, int $depth ): void {
Copy link
Contributor

@d4mation d4mation Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want an additional check to try to remove unnecessary parenthesis like this:

if (
	( ! empty( $this->transient_data['result_count'] ) )
	&& ( ! empty( $this->transient_data['total_count'] ) )
	&& ( $this->transient_data['result_count'] !== $this->transient_data['total_count'] )
) {

Copy link
Author

@nikolaystrikhar nikolaystrikhar Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed, good idea! One note though, we can't fully trust it and should always check how it changes the code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found some issues with indents and fixed them btw (it was mixing spaces and tabs)

…tter enforcement of coding standards. This update ensures that conditions with multiple boolean expressions and operators not at the start of the line are treated as errors, enhancing the automatic fixing capabilities of the sniff.
… standards enforcement. This class previously enforced the use of constants for post type slugs, but has been deemed unnecessary for current standards.
…cessary parentheses checks

- Introduced a new method `get_normalized_indent` to standardize indentation handling.
- Added functionality to check for unnecessary parentheses around single conditions, improving code clarity and adherence to coding standards.
- Updated indentation calculations for better alignment of closing parentheses and nested conditions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants