Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor/
.phpunit.result.cache
.php-cs-fixer.cache
/.claude/settings.local.json
462 changes: 462 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ parameters:
| **[ForbidExitExpressions](docs/ForbidExitExpressions.md)** | Detects and reports usage of exit and die expressions | Exit Expressions |
| **[ForbidGotoStatements](docs/ForbidGotoStatements.md)** | Detects and reports usage of goto statements | Goto Statements |
| **[NumberOfChildren](docs/NumberOfChildren.md)** | Detects classes with too many direct child classes | Class Hierarchy |
| **[TooManyMethods](docs/TooManyMethods.md)** | Detects classes with too many methods | Classes, Interfaces, Traits, Enums |

## 🔧 Configuration

Expand Down
14 changes: 13 additions & 1 deletion config/extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ parametersSchema:
ignored_in_classes: arrayOf(string()),
ignore_pattern: string(),
]),
too_many_methods: structure([
max_methods: int(),
ignore_pattern: string(),
]),
])

# default parameters
Expand Down Expand Up @@ -119,6 +123,9 @@ parameters:
boolean_argument_flag:
ignored_in_classes: []
ignore_pattern: ''
too_many_methods:
max_methods: 25
ignore_pattern: '^(get|set|is)'

services:
-
Expand Down Expand Up @@ -202,4 +209,9 @@ services:
factory: Orrison\MeliorStan\Rules\BooleanArgumentFlag\Config
arguments:
- %meliorstan.boolean_argument_flag.ignored_in_classes%
- %meliorstan.boolean_argument_flag.ignore_pattern%
- %meliorstan.boolean_argument_flag.ignore_pattern%
-
factory: Orrison\MeliorStan\Rules\TooManyMethods\Config
arguments:
- %meliorstan.too_many_methods.max_methods%
- %meliorstan.too_many_methods.ignore_pattern%
152 changes: 152 additions & 0 deletions docs/TooManyMethods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# TooManyMethods

This rule checks if a class, interface, trait, or enum has too many methods, which may indicate the class is doing too much and should be refactored.

Based on the [PHPMD TooManyMethods](https://phpmd.org/rules/codesize.html#toomanymethods) rule.

## Configuration

This rule supports the following configuration options:

### `max_methods`
- **Type**: `int`
- **Default**: `25`
- **Description**: The maximum number of methods allowed in a class-like structure before triggering an error.

### `ignore_pattern`
- **Type**: `string`
- **Default**: `^(get|set|is)`
- **Description**: A regular expression pattern (without delimiters) to match method names that should be excluded from the count. By default, getter, setter, and boolean accessor methods are ignored. Set to an empty string `''` to count all methods.

## Usage

Add the rule to your PHPStan configuration:

```neon
includes:
- vendor/orrison/meliorstan/config/extension.neon

rules:
- Orrison\MeliorStan\Rules\TooManyMethods\TooManyMethodsRule

parameters:
meliorstan:
too_many_methods:
max_methods: 25
ignore_pattern: '^(get|set|is)'
```

## Examples

### Default Configuration

```php
<?php

class UserService
{
// 10 getters - ignored by default
public function getName(): string { return ''; }
public function getEmail(): string { return ''; }
// ... more getters

// 5 setters - ignored by default
public function setName(string $name): void {}
// ... more setters

// 5 is* methods - ignored by default
public function isActive(): bool { return true; }
// ... more is* methods

// 10 regular methods - these are counted
public function process(): void {}
public function validate(): void {}
// ... more methods
}
// ✓ Valid - only 10 methods counted (getters/setters/is* ignored)

class GodClass
{
public function methodOne(): void {}
public function methodTwo(): void {}
// ... 26 total methods without get/set/is prefix
}
// ✗ Error: Class "GodClass" has 26 methods, which exceeds the maximum of 25. Consider refactoring.
```

### Configuration Examples

#### Custom Maximum

```neon
parameters:
meliorstan:
too_many_methods:
max_methods: 10
```

```php
<?php

class Service
{
public function methodOne(): void {}
public function methodTwo(): void {}
// ... 11 total methods
}
// ✗ Error: Class "Service" has 11 methods, which exceeds the maximum of 10.
```

#### Count All Methods (No Ignore Pattern)

```neon
parameters:
meliorstan:
too_many_methods:
ignore_pattern: ''
```

```php
<?php

class DataTransferObject
{
public function getName(): string { return ''; }
public function setName(string $name): void {}
public function getEmail(): string { return ''; }
public function setEmail(string $email): void {}
// ... 30 total methods including getters/setters
}
// ✗ Error: Class "DataTransferObject" has 30 methods, which exceeds the maximum of 25.
```

#### Custom Ignore Pattern

```neon
parameters:
meliorstan:
too_many_methods:
ignore_pattern: '^(get|set|is|has|with)'
```

```php
<?php

class Builder
{
public function getName(): string { return ''; } // ✓ Ignored
public function setName(string $n): void {} // ✓ Ignored
public function isValid(): bool { return true; } // ✓ Ignored
public function hasItems(): bool { return true; } // ✓ Ignored
public function withTimeout(int $t): self {} // ✓ Ignored
public function build(): object { return new \stdClass(); } // Counted
}
```

## Important Notes

- The rule applies to classes, interfaces, traits, and enums
- The `ignore_pattern` is case-insensitive (e.g., `getName` and `GETNAME` are both matched)
- Pattern delimiters (`/`) are added automatically - only provide the pattern itself
- Methods are counted based on their declaration in the specific class-like structure, not inherited methods
- Consider using this rule to identify classes that may benefit from being split into smaller, more focused classes (Single Responsibility Principle)
6 changes: 3 additions & 3 deletions src/Rules/BooleanArgumentFlag/BooleanArgumentFlagRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
*/
class BooleanArgumentFlagRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE_METHOD = 'Method "%s::%s()" has boolean parameter "$%s" which may indicate the method has multiple responsibilities.';
public const string ERROR_MESSAGE_TEMPLATE_METHOD = 'Method "%s::%s()" has boolean parameter "$%s" which may indicate the method has multiple responsibilities.';

public const ERROR_MESSAGE_TEMPLATE_FUNCTION = 'Function "%s()" has boolean parameter "$%s" which may indicate the function has multiple responsibilities.';
public const string ERROR_MESSAGE_TEMPLATE_FUNCTION = 'Function "%s()" has boolean parameter "$%s" which may indicate the function has multiple responsibilities.';

public const ERROR_MESSAGE_TEMPLATE_CLOSURE = 'Closure has boolean parameter "$%s" which may indicate the closure has multiple responsibilities.';
public const string ERROR_MESSAGE_TEMPLATE_CLOSURE = 'Closure has boolean parameter "$%s" which may indicate the closure has multiple responsibilities.';

public function __construct(
protected Config $config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
class BooleanGetMethodNameRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Method "%s" starts with "get" and returns boolean, consider using "is" or "has" instead.';
public const string ERROR_MESSAGE_TEMPLATE = 'Method "%s" starts with "get" and returns boolean, consider using "is" or "has" instead.';

public function __construct(
protected Config $config,
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/CamelCaseMethodName/CamelCaseMethodNameRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class CamelCaseMethodNameRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Method name "%s" is not in camelCase.';
public const string ERROR_MESSAGE_TEMPLATE = 'Method name "%s" is not in camelCase.';

/** @var array<string> */
protected array $ignoredMethods = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
class CamelCaseParameterNameRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Parameter name "%s" is not in camelCase.';
public const string ERROR_MESSAGE_TEMPLATE = 'Parameter name "%s" is not in camelCase.';

protected string $pattern;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class CamelCasePropertyNameRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Property name "%s" is not in camelCase.';
public const string ERROR_MESSAGE_TEMPLATE = 'Property name "%s" is not in camelCase.';

protected string $pattern;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class CamelCaseVariableNameRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Variable name "$%s" is not in camelCase.';
public const string ERROR_MESSAGE_TEMPLATE = 'Variable name "$%s" is not in camelCase.';

protected string $pattern;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class ConstantNamingConventionsRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Constant name "%s" is not in UPPERCASE.';
public const string ERROR_MESSAGE_TEMPLATE = 'Constant name "%s" is not in UPPERCASE.';

/**
* @return class-string<ClassConst>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class ConstructorWithNameAsEnclosingClassRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Method name "%s" is the same as the enclosing class "%s". This creates confusion as it resembles a PHP4-style constructor.';
public const string ERROR_MESSAGE_TEMPLATE = 'Method name "%s" is the same as the enclosing class "%s". This creates confusion as it resembles a PHP4-style constructor.';

/**
* @return class-string<Node>
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/ElseExpression/ElseExpressionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
class ElseExpressionRule implements Rule
{
public const ERROR_MESSAGE = 'Avoid using else expressions.';
public const string ERROR_MESSAGE = 'Avoid using else expressions.';

public function __construct(
protected Config $config,
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/EmptyCatchBlock/EmptyCatchBlockRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
class EmptyCatchBlockRule implements Rule
{
public const ERROR_MESSAGE = 'Empty catch block detected. Catch blocks should contain error handling logic.';
public const string ERROR_MESSAGE = 'Empty catch block detected. Catch blocks should contain error handling logic.';

public function getNodeType(): string
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
class ForbidCountInLoopExpressionsRule implements Rule
{
public const ERROR_MESSAGE = 'Using count() or sizeof() in loop conditions can cause performance issues or hard to trace bugs.';
public const string ERROR_MESSAGE = 'Using count() or sizeof() in loop conditions can cause performance issues or hard to trace bugs.';

public function getNodeType(): string
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
class ForbidEvalExpressionsRule implements Rule
{
public const ERROR_MESSAGE = 'Eval expressions should not be used.';
public const string ERROR_MESSAGE = 'Eval expressions should not be used.';

public function getNodeType(): string
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
class ForbidExitExpressionsRule implements Rule
{
public const ERROR_MESSAGE = 'Exit expressions should not be used.';
public const string ERROR_MESSAGE = 'Exit expressions should not be used.';

public function getNodeType(): string
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
class ForbidGotoStatementsRule implements Rule
{
public const ERROR_MESSAGE = 'Goto statements should not be used.';
public const string ERROR_MESSAGE = 'Goto statements should not be used.';

public function getNodeType(): string
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/ForbidPestPhpOnly/ForbidPestPhpOnlyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
class ForbidPestPhpOnlyRule implements Rule
{
public const ERROR_MESSAGE = 'Pest\'s only() filter should not be used in committed tests.';
public const string ERROR_MESSAGE = 'Pest\'s only() filter should not be used in committed tests.';

private const PEST_ENTRY_POINTS = [
'test',
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/LongClassName/LongClassNameRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class LongClassNameRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = '%s name "%s" is too long (%d chars). Maximum allowed length is %d characters.';
public const string ERROR_MESSAGE_TEMPLATE = '%s name "%s" is too long (%d chars). Maximum allowed length is %d characters.';

public function __construct(
protected Config $config,
Expand Down
6 changes: 3 additions & 3 deletions src/Rules/LongVariable/LongVariableRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
*/
class LongVariableRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE_PARAMETER = 'Parameter name "$%s" is %d characters long, which exceeds the maximum of %d characters.';
public const string ERROR_MESSAGE_TEMPLATE_PARAMETER = 'Parameter name "$%s" is %d characters long, which exceeds the maximum of %d characters.';

public const ERROR_MESSAGE_TEMPLATE_PROPERTY = 'Property name "$%s" is %d characters long, which exceeds the maximum of %d characters.';
public const string ERROR_MESSAGE_TEMPLATE_PROPERTY = 'Property name "$%s" is %d characters long, which exceeds the maximum of %d characters.';

public const ERROR_MESSAGE_TEMPLATE_VARIABLE = 'Variable name "$%s" is %d characters long, which exceeds the maximum of %d characters.';
public const string ERROR_MESSAGE_TEMPLATE_VARIABLE = 'Variable name "$%s" is %d characters long, which exceeds the maximum of %d characters.';

/** @var array<string, int> Track variables processed in special contexts by name and line */
protected array $specialContextVariables = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
class MissingClosureParameterTypehintRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Parameter #%d $%s of anonymous function has no typehint.';
public const string ERROR_MESSAGE_TEMPLATE = 'Parameter #%d $%s of anonymous function has no typehint.';

/**
* @return class-string<Node>
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/NumberOfChildren/NumberOfChildrenRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
class NumberOfChildrenRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Class "%s" has %d direct children, exceeding the maximum of %d.';
public const string ERROR_MESSAGE_TEMPLATE = 'Class "%s" has %d direct children, exceeding the maximum of %d.';

public function __construct(
private Config $config
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/PascalCaseClassName/PascalCaseClassNameRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
class PascalCaseClassNameRule implements Rule
{
public const ERROR_MESSAGE_TEMPLATE = 'Class name "%s" is not in PascalCase.';
public const string ERROR_MESSAGE_TEMPLATE = 'Class name "%s" is not in PascalCase.';

public function __construct(
protected Config $config,
Expand Down
Loading