From 665aa6870d0091e763242778ac9d82942b15fd60 Mon Sep 17 00:00:00 2001 From: Kevin Ullyott Date: Tue, 30 Dec 2025 22:06:31 -0500 Subject: [PATCH 1/6] Build claude.md file --- CLAUDE.md | 462 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f6155bc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,462 @@ +# MeliorStan - AI Coding Instructions + +## Project Overview +This is a PHPStan extension that provides different custom PHPstan rules with configurable options. Each rule has its own namespace under `src/Rules/` and follows a consistent architecture pattern. +Though many of the rules are focused on providing similar functionality to PHP Mess Detector. (https://phpmd.org/) There will also be some rules that are unique to this extension. + +## Architecture Pattern +Each rule follows a 3-component structure: +- `{RuleName}Rule.php` - The main rule implementing `Rule` +- `Config.php` - Configuration class with boolean flags for rule options. This class is used to define the configuration options for the rule. Not all rules have configuration options, but those that do will have a `Config` class this that do not will not. +- Test files in `tests/Rules/{RuleName}/` with multiple configuration scenarios if applicable. +- Unlike most other PHPStan extensions, this one does not use a single monolithic rule class. Each rule is its own class with its own config. And a User would enable/disable each rule individually in their PHPStan config. We do not register all rules for the user in our extension.neon. + +### Key Files +- `config/extension.neon` - Central dependency injection & parameter schema definition +- `tests/Rules/*/config/*.neon` - Per-test configuration files that override defaults +- `tests/Rules/*/Fixture/ExampleClass.php` - Test fixtures with various naming patterns + - Though this is the primary fixture we have used so far, feel free to add more or different fixture files if needed for specific test cases. + +## Critical Development Workflows + +### Testing +```bash +composer test # Run all tests with paratest +./vendor/bin/phpunit tests/Rules/SpecificRule/ # Test specific rule +``` + +### Code Style +```bash +composer format # Auto-fix code style with PHP-CS-Fixer +composer analyze # Run PHPStan analysis +``` + +### Running Final Checks +Before finalizing changes, you need to ensure that formatting has been applied, static analysis is passing, and all tests are successful. It is important to run these checks in order. First check formatting, then static analysis, and finally run all tests. This is because formatting may change line numbers in tests, which can cause test failures if not done first. + +## Configuration Architecture +The extension uses Neon dependency injection with a hierarchical configuration system: + +1. **Schema Definition** (`config/extension.neon`): Defines parameter structure and defaults +2. **Test Overrides** (`tests/Rules/*/config/*.neon`): Override specific parameters for test scenarios +3. **Service Registration**: Config classes are auto-wired using parameter references + +### Critical Pattern: Config Parameter Mapping +In `config/extension.neon`, ensure Config service arguments match the correct parameter namespace: +```neon +- factory: Orrison\MeliorStan\Rules\CamelCaseParameterName\Config + arguments: + - %meliorstan.camel_case_parameter_name.allow_consecutive_uppercase% # Not property_name! + - %meliorstan.camel_case_parameter_name.allow_underscore_prefix% +``` + +### Rule Registration for New Rules +When creating a new rule, you must add BOTH schema definition AND service registration to `config/extension.neon`: + +1. **Add parameter schema** (in `parametersSchema` section): +```neon +parametersSchema: + meliorstan: structure([ + # ... existing rules + new_rule_name: structure([ + config_option: bool(), + ]), + ]) +``` + +2. **Add default parameters** (in `parameters` section): +```neon +parameters: + meliorstan: + # ... existing rules + new_rule_name: + config_option: false +``` + +3. **Add service registration** (in `services` section): +```neon +services: + # ... existing services + - + factory: Orrison\MeliorStan\Rules\NewRuleName\Config + arguments: + - %meliorstan.new_rule_name.config_option% +``` + +**IMPORTANT**: The main rule class itself is NOT registered in extension.neon. Users must register the rule in their own PHPStan configuration. + +### Test Configuration Files +Test config files must include BOTH the extension config AND rule registration: + +```neon +includes: + - ../../../../config/extension.neon + +rules: + - Orrison\MeliorStan\Rules\NewRuleName\NewRuleNameRule + +parameters: + meliorstan: + new_rule_name: + config_option: true # Override for this test +``` + +**Critical**: Without the `rules:` section, tests will fail with "Service of type ... not found" error. + +### Config Parameter Naming Conventions +When adding new rules to `config/extension.neon`, follow these naming patterns: + +1. **Schema keys**: Use snake_case matching the rule directory name: + ```neon + boolean_get_method_name: structure([ # Directory: BooleanGetMethodName + check_parameterized_methods: bool(), + ]) + ``` + +2. **Parameter references**: Must match the schema key exactly: + ```neon + - factory: Orrison\MeliorStan\Rules\BooleanGetMethodName\Config + arguments: + - %meliorstan.boolean_get_method_name.check_parameterized_methods% + ``` + +3. **Config method names**: Use camelCase with "get" prefix: + ```php + public function getCheckParameterizedMethods(): bool + ``` + +## Rule Implementation Patterns + +### Node Type Selection +- **ClassMethod**: Use `ClassMethod::class` for method name rules +- **Property**: Use `Property::class` for property rules (iterate `$node->props`) +- **Param**: Use `Param::class` for parameter rules (check `$node->var->name`) +- **Class_**: Use `Class_::class` for class name rules +- For all other new nodes you can scan `vendor/nikic/php-parser/lib/PhpParser/Node` + +### Regex Pattern Building +Rules build patterns dynamically based on config flags, example: +```php +$pattern = '/^'; +$pattern .= $this->config->getAllowUnderscorePrefix() ? '_?' : ''; +$pattern .= '[a-z]'; +$pattern .= $this->config->getAllowConsecutiveUppercase() + ? '[a-zA-Z0-9]*' + : '(?:[a-z0-9]+|[A-Z][a-z0-9]+)*'; +$pattern .= '$/'; +``` + +## Test Conventions + +### Test Structure +Each rule has multiple test classes covering all possible different configuration combinations, examples are: +- `DefaultOptionsTest` - Default configuration +- `AllowConsecutiveUppercaseTest` - One option enabled +- `AllOptionsTrueTest` - All options enabled +- `Allow{Specific}Test` - Single feature tests + +### Test Expectations +Tests use exact line numbers from fixture files. When fixture formatting changes, update expected line numbers in test assertions: +```php +['Parameter name "is_http_response" is not in camelCase.', 5], +``` + +### Common Test Issues +- **Wrong line numbers**: Update after code formatting changes +- **Wrong error messages**: Ensure "Parameter name" vs "Property name" vs "Method name" matches rule type +- **Config mismatch**: Verify test config file parameters match the rule being tested + +### Critical: Line Number Management After Formatting +**ALWAYS run `composer format` BEFORE finalizing test line numbers!** + +1. Write initial tests with approximate line numbers +2. Run `composer format` to apply code style fixes +3. Run the specific test to see actual vs expected line numbers +4. Update test assertions with correct line numbers from the failing test output +5. Re-run tests to verify they pass + +**Example workflow:** +```bash +# After creating fixture and test files +composer format # Format all code first +./vendor/bin/phpunit tests/Rules/NewRule/DefaultTest.php # See line number errors +# Update test with correct line numbers from failure output +./vendor/bin/phpunit tests/Rules/NewRule/DefaultTest.php # Verify passes +``` + +The formatter often adds PHPDoc annotations and adjusts spacing, which changes line numbers. Doing this early prevents having to fix line numbers multiple times. + +## Project-Specific Patterns + +### Class Extensibility +- No classes should be marked as `final` unless absolutely necessary. This allows for easier extension and customization in the future. +- No methods or properties should be marked as `private` unless absolutely necessary. Use `protected` or `public` to allow for subclass overrides. + - Use `protected` for methods that are intended to be overridden in subclasses, and `public` for methods that are part of the public API of the rule. +- It is okay to use `final` on classes or `private` on methods in tests if the intent is to prove that the rule works as expected in a specific scenario or if the rule is doing something specific with final classes and private methods/properties, but this should be avoided in the source code of our rules + +### Namespace Convention +All classes use `Orrison\MeliorStan\` prefix with rule-specific sub-namespaces. + +### Declare Strict Types +Do NOT add `declare(strict_types=1);` to the top of files. This is not a requirement for this project and we do not want it. Unless the rule specifically has to do with this strict type declaration, then it is not needed. This is to keep the codebase consistent and avoid unnecessary complexity. + +### Import Statements +Always use full imports for all classes and interfaces used in the file. Do not use fully qualified class names in the code - import them explicitly at the top of the file instead. This includes: +- PHPStan classes (`PHPStan\Analyser\Scope`, `PHPStan\Rules\Rule`, etc.) +- PhpParser node classes (`PhpParser\Node`, `PhpParser\Node\Stmt\Class_`, `PhpParser\Node\Identifier`, etc.) +- All other external dependencies + +Example: +```php +use PhpParser\Node; +use PhpParser\Node\Identifier; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassLike; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleErrorBuilder; +``` + +### Error Message Constants +**CRITICAL**: All rule error messages MUST be defined as public constants in the rule class. This improves maintainability, consistency, and makes error messages the single source of truth. + +#### Static Error Messages +For rules with a single, unchanging error message, use a simple constant: + +```php +class EmptyCatchBlockRule implements Rule +{ + public const ERROR_MESSAGE = 'Empty catch block is not allowed.'; + + public function processNode(Node $node, Scope $scope): array + { + // ... validation logic ... + + return [ + RuleErrorBuilder::message(self::ERROR_MESSAGE) + ->identifier('MeliorStan.emptyCatchBlock') + ->build(), + ]; + } +} +``` + +#### Dynamic Error Messages (Single Template) +For rules where the error message includes dynamic data (e.g., variable names, lengths), use a template constant with placeholders for `sprintf()`: + +```php +class ShortMethodNameRule implements Rule +{ + public const ERROR_MESSAGE_TEMPLATE = 'Method name "%s" is shorter than minimum length of %d characters.'; + + public function processNode(Node $node, Scope $scope): array + { + // ... validation logic ... + + return [ + RuleErrorBuilder::message( + sprintf(self::ERROR_MESSAGE_TEMPLATE, $methodName, $minimumLength) + ) + ->identifier('MeliorStan.methodNameTooShort') + ->build(), + ]; + } +} +``` + +#### Multiple Error Message Templates +For rules that validate different contexts (e.g., properties, parameters, and variables), use separate template constants for each context: + +```php +class ShortVariableRule implements Rule +{ + public const ERROR_MESSAGE_TEMPLATE_PROPERTY = 'Property name "$%s" is shorter than minimum length of %d characters.'; + public const ERROR_MESSAGE_TEMPLATE_PARAMETER = 'Parameter name "$%s" is shorter than minimum length of %d characters.'; + public const ERROR_MESSAGE_TEMPLATE_VARIABLE = 'Variable name "$%s" is shorter than minimum length of %d characters.'; + + public function processNode(Node $node, Scope $scope): array + { + // ... determine context (property, parameter, or variable) ... + + if ($isProperty) { + $message = sprintf(self::ERROR_MESSAGE_TEMPLATE_PROPERTY, $name, $minLength); + } elseif ($isParameter) { + $message = sprintf(self::ERROR_MESSAGE_TEMPLATE_PARAMETER, $name, $minLength); + } else { + $message = sprintf(self::ERROR_MESSAGE_TEMPLATE_VARIABLE, $name, $minLength); + } + + return [ + RuleErrorBuilder::message($message) + ->identifier('MeliorStan.variableNameTooShort') + ->build(), + ]; + } +} +``` + +#### Test Implementation +Tests MUST use the constants instead of hardcoded strings: + +**Static messages:** +```php +$this->analyse([__DIR__ . '/Fixture/Example.php'], [ + [EmptyCatchBlockRule::ERROR_MESSAGE, 14], + [EmptyCatchBlockRule::ERROR_MESSAGE, 23], +]); +``` + +**Dynamic messages with single template:** +```php +$this->analyse([__DIR__ . '/Fixture/Example.php'], [ + [sprintf(ShortMethodNameRule::ERROR_MESSAGE_TEMPLATE, 'a', 3), 7], + [sprintf(ShortMethodNameRule::ERROR_MESSAGE_TEMPLATE, 'ab', 3), 10], +]); +``` + +**Dynamic messages with multiple templates:** +```php +$this->analyse([__DIR__ . '/Fixture/Example.php'], [ + [sprintf(ShortVariableRule::ERROR_MESSAGE_TEMPLATE_PROPERTY, 'x', 3), 9], + [sprintf(ShortVariableRule::ERROR_MESSAGE_TEMPLATE_PARAMETER, 'a', 3), 15], + [sprintf(ShortVariableRule::ERROR_MESSAGE_TEMPLATE_VARIABLE, 'b', 3), 17], +]); +``` + +**IMPORTANT**: +- Never use hardcoded error message strings in tests +- Variable names in sprintf should NOT include the `$` prefix (the template handles that) +- Constants should be `public` for test access +- Use descriptive constant names that indicate the message type + +### Error Identifiers +Use consistent identifiers for rule errors: +```php +->identifier('MeliorStan.methodNameNotCamelCase') +``` + +### Config Naming +Config methods use descriptive names with `get` prefix and follow camelCase: +- `getAllowConsecutiveUppercase()` +- `getAllowUnderscorePrefix()` +- `getAllowUnderscoreInTests()` + +## Common Pitfalls +1. **Config Parameter Mismatch**: Ensure service arguments reference correct parameter namespace in `extension.neon` +2. **Test Line Numbers**: Always verify line numbers match fixture file after formatting +3. **Node Type Confusion**: Use correct node type for the syntax element being validated +4. **Missing Magic Method Exclusion**: Don't forget to exclude PHP magic methods in method rules +5. **Hardcoded Error Messages**: Always use public constants for error messages in rule classes, never hardcode strings in tests or multiple places in the rule + +## Contribution Guidelines +- Follow the coding standards of the project, styling defined by PHP-CS-Fixer configuration. `php-cs-fixer.php` is the config file. And `composer format` will auto-fix code style. And linting/static analysis can be run with `composer analyze` and is configured with our PHPStan configuration `phpstan.neon.dist`. +- All new rules should follow the established 3-component structure (Rule, Config, Tests) unless there's a compelling reason or asked to deviate. +- **All error messages must be defined as public constants** in the rule class (ERROR_MESSAGE, ERROR_MESSAGE_TEMPLATE, or ERROR_MESSAGE_TEMPLATE_[CONTEXT]) +- **Tests must use these constants** instead of hardcoded error message strings +- When adding new rules, ensure to add appropriate test cases covering all configuration permutations. +- When updating existing rules, ensure backward compatibility unless a breaking change is explicitly intended and documented. +- When done with changes, ensure that you MUST run formatting and static analysis, then run all tests. You must keep doing this till all three pass and it MUST be done in this order as formatting may change line numbers in tests. + +## Documentation Standards + +### Documentation Requirements +- **MANDATORY**: All rules MUST have comprehensive documentation in `docs/{RuleName}.md` +- **README Updates**: The main `README.md` must be updated to link to new rule documentation +- Documentation must be created/updated whenever: + - Adding new rules + - Modifying existing rule behavior + - Adding or changing configuration options + - Changing rule names or namespaces + +### Documentation Structure +Each rule documentation file must follow this exact structure: + +```markdown +# {Rule name without the "Rule" suffix, e.g. CamelCaseVariableName} + +{Brief description of what the rule enforces} + +{Detailed description explaining the rule's purpose and scope} + +## Configuration + +This rule supports the following configuration options: + +### `config_option_name` +- **Type**: `bool` (or appropriate type) +- **Default**: `false` (or appropriate default) +- **Description**: {Clear description of what this option enables/disables with example} + +{If a rule does not have any configurable options, include a note here stating "This rule has no configuration options."} + +## Usage + +Add the rule to your PHPStan configuration: + +```neon +includes: + - vendor/orrison/meliorstan/config/extension.neon + +rules: + - Orrison\MeliorStan\Rules\{Namespace}\{RuleName}Rule + +parameters: + meliorstan: + {config_namespace}: + config_option: false +``` + +## Examples + +### Default Configuration + +```php + Date: Tue, 30 Dec 2025 22:37:42 -0500 Subject: [PATCH 2/6] Create the TooManyMethodsRule --- README.md | 1 + config/extension.neon | 14 +- docs/TooManyMethods.md | 152 +++++++ src/Rules/TooManyMethods/Config.php | 21 + .../TooManyMethods/TooManyMethodsRule.php | 112 +++++ .../TooManyMethods/CustomMaxMethodsTest.php | 58 +++ .../TooManyMethods/Fixture/ExampleClass.php | 394 ++++++++++++++++++ .../TooManyMethods/NoIgnorePatternTest.php | 54 +++ .../TooManyMethods/TooManyMethodsRuleTest.php | 48 +++ .../config/configured_rule.neon | 5 + .../config/custom_max_methods.neon | 10 + .../config/no_ignore_pattern.neon | 10 + 12 files changed, 878 insertions(+), 1 deletion(-) create mode 100644 docs/TooManyMethods.md create mode 100644 src/Rules/TooManyMethods/Config.php create mode 100644 src/Rules/TooManyMethods/TooManyMethodsRule.php create mode 100644 tests/Rules/TooManyMethods/CustomMaxMethodsTest.php create mode 100644 tests/Rules/TooManyMethods/Fixture/ExampleClass.php create mode 100644 tests/Rules/TooManyMethods/NoIgnorePatternTest.php create mode 100644 tests/Rules/TooManyMethods/TooManyMethodsRuleTest.php create mode 100644 tests/Rules/TooManyMethods/config/configured_rule.neon create mode 100644 tests/Rules/TooManyMethods/config/custom_max_methods.neon create mode 100644 tests/Rules/TooManyMethods/config/no_ignore_pattern.neon diff --git a/README.md b/README.md index afde10f..67b7c54 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config/extension.neon b/config/extension.neon index 9033734..685c35f 100644 --- a/config/extension.neon +++ b/config/extension.neon @@ -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 @@ -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: - @@ -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% \ No newline at end of file + - %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% \ No newline at end of file diff --git a/docs/TooManyMethods.md b/docs/TooManyMethods.md new file mode 100644 index 0000000..d3f35ed --- /dev/null +++ b/docs/TooManyMethods.md @@ -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 +maxMethods; + } + + public function getIgnorePattern(): string + { + return $this->ignorePattern; + } +} diff --git a/src/Rules/TooManyMethods/TooManyMethodsRule.php b/src/Rules/TooManyMethods/TooManyMethodsRule.php new file mode 100644 index 0000000..820285e --- /dev/null +++ b/src/Rules/TooManyMethods/TooManyMethodsRule.php @@ -0,0 +1,112 @@ + + */ +class TooManyMethodsRule implements Rule +{ + public const string ERROR_MESSAGE_TEMPLATE = '%s "%s" has %d methods, which exceeds the maximum of %d. Consider refactoring.'; + + public function __construct( + protected Config $config, + ) {} + + /** + * @return class-string + */ + public function getNodeType(): string + { + return ClassLike::class; + } + + /** + * @param ClassLike $node + * @return RuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $node->name instanceof Identifier) { + return []; + } + + $methodCount = $this->countMethods($node); + + if ($methodCount <= $this->config->getMaxMethods()) { + return []; + } + + $nodeType = $this->getNodeTypeName($node); + $nodeName = $node->name->toString(); + + return [ + RuleErrorBuilder::message( + sprintf( + self::ERROR_MESSAGE_TEMPLATE, + $nodeType, + $nodeName, + $methodCount, + $this->config->getMaxMethods() + ) + ) + ->identifier('MeliorStan.tooManyMethods') + ->build(), + ]; + } + + protected function countMethods(ClassLike $node): int + { + $ignorePattern = $this->config->getIgnorePattern(); + + if ($ignorePattern === '') { + return count($node->getMethods()); + } + + $regex = '/' . $ignorePattern . '/i'; + $count = 0; + + foreach ($node->getMethods() as $method) { + $methodName = $method->name->toString(); + + if (preg_match($regex, $methodName) === 0) { + ++$count; + } + } + + return $count; + } + + protected function getNodeTypeName(ClassLike $node): string + { + if ($node instanceof Class_) { + return 'Class'; + } + + if ($node instanceof Interface_) { + return 'Interface'; + } + + if ($node instanceof Trait_) { + return 'Trait'; + } + + if ($node instanceof Enum_) { + return 'Enum'; + } + + return 'Class'; + } +} diff --git a/tests/Rules/TooManyMethods/CustomMaxMethodsTest.php b/tests/Rules/TooManyMethods/CustomMaxMethodsTest.php new file mode 100644 index 0000000..871b73d --- /dev/null +++ b/tests/Rules/TooManyMethods/CustomMaxMethodsTest.php @@ -0,0 +1,58 @@ + + */ +class CustomMaxMethodsTest extends RuleTestCase +{ + public function testRule(): void + { + $this->analyse([__DIR__ . '/Fixture/ExampleClass.php'], [ + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Class', 'ClassWithManyMethods', 10, 5), + 15, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Class', 'ClassExceedingLimit', 26, 5), + 127, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Trait', 'TraitExceedingLimit', 26, 5), + 203, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Interface', 'InterfaceExceedingLimit', 26, 5), + 262, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Enum', 'EnumExceedingLimit', 26, 5), + 321, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Class', 'ClassWithSixMethods', 6, 5), + 381, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/custom_max_methods.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(TooManyMethodsRule::class); + } +} diff --git a/tests/Rules/TooManyMethods/Fixture/ExampleClass.php b/tests/Rules/TooManyMethods/Fixture/ExampleClass.php new file mode 100644 index 0000000..0460785 --- /dev/null +++ b/tests/Rules/TooManyMethods/Fixture/ExampleClass.php @@ -0,0 +1,394 @@ + + */ +class NoIgnorePatternTest extends RuleTestCase +{ + public function testRule(): void + { + $this->analyse([__DIR__ . '/Fixture/ExampleClass.php'], [ + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Class', 'ClassWithManyMethods', 30, 25), + 15, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Class', 'ClassExceedingLimit', 26, 25), + 127, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Trait', 'TraitExceedingLimit', 26, 25), + 203, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Interface', 'InterfaceExceedingLimit', 26, 25), + 262, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Enum', 'EnumExceedingLimit', 26, 25), + 321, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/no_ignore_pattern.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(TooManyMethodsRule::class); + } +} diff --git a/tests/Rules/TooManyMethods/TooManyMethodsRuleTest.php b/tests/Rules/TooManyMethods/TooManyMethodsRuleTest.php new file mode 100644 index 0000000..9b3d6ea --- /dev/null +++ b/tests/Rules/TooManyMethods/TooManyMethodsRuleTest.php @@ -0,0 +1,48 @@ + + */ +class TooManyMethodsRuleTest extends RuleTestCase +{ + public function testRule(): void + { + $this->analyse([__DIR__ . '/Fixture/ExampleClass.php'], [ + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Class', 'ClassExceedingLimit', 26, 25), + 127, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Trait', 'TraitExceedingLimit', 26, 25), + 203, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Interface', 'InterfaceExceedingLimit', 26, 25), + 262, + ], + [ + sprintf(TooManyMethodsRule::ERROR_MESSAGE_TEMPLATE, 'Enum', 'EnumExceedingLimit', 26, 25), + 321, + ], + ]); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/configured_rule.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(TooManyMethodsRule::class); + } +} diff --git a/tests/Rules/TooManyMethods/config/configured_rule.neon b/tests/Rules/TooManyMethods/config/configured_rule.neon new file mode 100644 index 0000000..9c23a32 --- /dev/null +++ b/tests/Rules/TooManyMethods/config/configured_rule.neon @@ -0,0 +1,5 @@ +includes: + - ../../../../config/extension.neon + +rules: + - Orrison\MeliorStan\Rules\TooManyMethods\TooManyMethodsRule diff --git a/tests/Rules/TooManyMethods/config/custom_max_methods.neon b/tests/Rules/TooManyMethods/config/custom_max_methods.neon new file mode 100644 index 0000000..5402475 --- /dev/null +++ b/tests/Rules/TooManyMethods/config/custom_max_methods.neon @@ -0,0 +1,10 @@ +includes: + - ../../../../config/extension.neon + +rules: + - Orrison\MeliorStan\Rules\TooManyMethods\TooManyMethodsRule + +parameters: + meliorstan: + too_many_methods: + max_methods: 5 diff --git a/tests/Rules/TooManyMethods/config/no_ignore_pattern.neon b/tests/Rules/TooManyMethods/config/no_ignore_pattern.neon new file mode 100644 index 0000000..cbca63d --- /dev/null +++ b/tests/Rules/TooManyMethods/config/no_ignore_pattern.neon @@ -0,0 +1,10 @@ +includes: + - ../../../../config/extension.neon + +rules: + - Orrison\MeliorStan\Rules\TooManyMethods\TooManyMethodsRule + +parameters: + meliorstan: + too_many_methods: + ignore_pattern: '' From 21b574fa555082de6576d4a5e266da5202ba0226 Mon Sep 17 00:00:00 2001 From: Kevin Ullyott Date: Tue, 30 Dec 2025 22:41:19 -0500 Subject: [PATCH 3/6] Gitignore some claude files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1e13ae2..580884b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor/ .phpunit.result.cache .php-cs-fixer.cache +/.claude/settings.local.json From 0d57e4d82b575a737693c6d4cc257e50f6c8f6f9 Mon Sep 17 00:00:00 2001 From: Kevin Ullyott Date: Tue, 30 Dec 2025 22:59:15 -0500 Subject: [PATCH 4/6] Fix formatting --- src/Rules/TooManyMethods/TooManyMethodsRule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Rules/TooManyMethods/TooManyMethodsRule.php b/src/Rules/TooManyMethods/TooManyMethodsRule.php index 820285e..1324e4c 100644 --- a/src/Rules/TooManyMethods/TooManyMethodsRule.php +++ b/src/Rules/TooManyMethods/TooManyMethodsRule.php @@ -35,6 +35,7 @@ public function getNodeType(): string /** * @param ClassLike $node + * * @return RuleError[] */ public function processNode(Node $node, Scope $scope): array From 45734e6d1f2a07be750a3ed35db7542cc18ac26c Mon Sep 17 00:00:00 2001 From: Kevin Ullyott Date: Tue, 30 Dec 2025 23:22:25 -0500 Subject: [PATCH 5/6] Type-hint ERROR_MESSAGE_TEMPLATE consts --- src/Rules/BooleanArgumentFlag/BooleanArgumentFlagRule.php | 6 +++--- src/Rules/BooleanGetMethodName/BooleanGetMethodNameRule.php | 2 +- src/Rules/CamelCaseMethodName/CamelCaseMethodNameRule.php | 2 +- .../CamelCaseParameterName/CamelCaseParameterNameRule.php | 2 +- .../CamelCasePropertyName/CamelCasePropertyNameRule.php | 2 +- .../CamelCaseVariableName/CamelCaseVariableNameRule.php | 2 +- .../ConstantNamingConventionsRule.php | 2 +- .../ConstructorWithNameAsEnclosingClassRule.php | 2 +- src/Rules/ElseExpression/ElseExpressionRule.php | 2 +- src/Rules/EmptyCatchBlock/EmptyCatchBlockRule.php | 2 +- .../ForbidCountInLoopExpressionsRule.php | 2 +- .../ForbidEvalExpressions/ForbidEvalExpressionsRule.php | 2 +- .../ForbidExitExpressions/ForbidExitExpressionsRule.php | 2 +- src/Rules/ForbidGotoStatements/ForbidGotoStatementsRule.php | 2 +- src/Rules/ForbidPestPhpOnly/ForbidPestPhpOnlyRule.php | 2 +- src/Rules/LongClassName/LongClassNameRule.php | 2 +- src/Rules/LongVariable/LongVariableRule.php | 6 +++--- .../MissingClosureParameterTypehintRule.php | 2 +- src/Rules/NumberOfChildren/NumberOfChildrenRule.php | 2 +- src/Rules/PascalCaseClassName/PascalCaseClassNameRule.php | 2 +- src/Rules/ShortClassName/ShortClassNameRule.php | 2 +- src/Rules/ShortMethodName/ShortMethodNameRule.php | 2 +- src/Rules/ShortVariable/ShortVariableRule.php | 6 +++--- src/Rules/Superglobals/SuperglobalsRule.php | 2 +- .../TraitConstantNamingConventionsRule.php | 2 +- 25 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Rules/BooleanArgumentFlag/BooleanArgumentFlagRule.php b/src/Rules/BooleanArgumentFlag/BooleanArgumentFlagRule.php index 404d906..c0b88ac 100644 --- a/src/Rules/BooleanArgumentFlag/BooleanArgumentFlagRule.php +++ b/src/Rules/BooleanArgumentFlag/BooleanArgumentFlagRule.php @@ -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, diff --git a/src/Rules/BooleanGetMethodName/BooleanGetMethodNameRule.php b/src/Rules/BooleanGetMethodName/BooleanGetMethodNameRule.php index 84bf624..59ce891 100644 --- a/src/Rules/BooleanGetMethodName/BooleanGetMethodNameRule.php +++ b/src/Rules/BooleanGetMethodName/BooleanGetMethodNameRule.php @@ -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, diff --git a/src/Rules/CamelCaseMethodName/CamelCaseMethodNameRule.php b/src/Rules/CamelCaseMethodName/CamelCaseMethodNameRule.php index c4a5656..5468a3b 100644 --- a/src/Rules/CamelCaseMethodName/CamelCaseMethodNameRule.php +++ b/src/Rules/CamelCaseMethodName/CamelCaseMethodNameRule.php @@ -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 */ protected array $ignoredMethods = [ diff --git a/src/Rules/CamelCaseParameterName/CamelCaseParameterNameRule.php b/src/Rules/CamelCaseParameterName/CamelCaseParameterNameRule.php index e3ed047..bf3a141 100644 --- a/src/Rules/CamelCaseParameterName/CamelCaseParameterNameRule.php +++ b/src/Rules/CamelCaseParameterName/CamelCaseParameterNameRule.php @@ -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; diff --git a/src/Rules/CamelCasePropertyName/CamelCasePropertyNameRule.php b/src/Rules/CamelCasePropertyName/CamelCasePropertyNameRule.php index 26300bd..db8cd17 100644 --- a/src/Rules/CamelCasePropertyName/CamelCasePropertyNameRule.php +++ b/src/Rules/CamelCasePropertyName/CamelCasePropertyNameRule.php @@ -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; diff --git a/src/Rules/CamelCaseVariableName/CamelCaseVariableNameRule.php b/src/Rules/CamelCaseVariableName/CamelCaseVariableNameRule.php index 86319a7..dee812d 100644 --- a/src/Rules/CamelCaseVariableName/CamelCaseVariableNameRule.php +++ b/src/Rules/CamelCaseVariableName/CamelCaseVariableNameRule.php @@ -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; diff --git a/src/Rules/ConstantNamingConventions/ConstantNamingConventionsRule.php b/src/Rules/ConstantNamingConventions/ConstantNamingConventionsRule.php index 940c1a5..fbc915f 100644 --- a/src/Rules/ConstantNamingConventions/ConstantNamingConventionsRule.php +++ b/src/Rules/ConstantNamingConventions/ConstantNamingConventionsRule.php @@ -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 diff --git a/src/Rules/ConstructorWithNameAsEnclosingClass/ConstructorWithNameAsEnclosingClassRule.php b/src/Rules/ConstructorWithNameAsEnclosingClass/ConstructorWithNameAsEnclosingClassRule.php index a4db259..f4819d7 100644 --- a/src/Rules/ConstructorWithNameAsEnclosingClass/ConstructorWithNameAsEnclosingClassRule.php +++ b/src/Rules/ConstructorWithNameAsEnclosingClass/ConstructorWithNameAsEnclosingClassRule.php @@ -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 diff --git a/src/Rules/ElseExpression/ElseExpressionRule.php b/src/Rules/ElseExpression/ElseExpressionRule.php index c582a87..42f526c 100644 --- a/src/Rules/ElseExpression/ElseExpressionRule.php +++ b/src/Rules/ElseExpression/ElseExpressionRule.php @@ -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, diff --git a/src/Rules/EmptyCatchBlock/EmptyCatchBlockRule.php b/src/Rules/EmptyCatchBlock/EmptyCatchBlockRule.php index 7a34d38..db31ec5 100644 --- a/src/Rules/EmptyCatchBlock/EmptyCatchBlockRule.php +++ b/src/Rules/EmptyCatchBlock/EmptyCatchBlockRule.php @@ -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 { diff --git a/src/Rules/ForbidCountInLoopExpressions/ForbidCountInLoopExpressionsRule.php b/src/Rules/ForbidCountInLoopExpressions/ForbidCountInLoopExpressionsRule.php index 3469384..83ed787 100644 --- a/src/Rules/ForbidCountInLoopExpressions/ForbidCountInLoopExpressionsRule.php +++ b/src/Rules/ForbidCountInLoopExpressions/ForbidCountInLoopExpressionsRule.php @@ -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 { diff --git a/src/Rules/ForbidEvalExpressions/ForbidEvalExpressionsRule.php b/src/Rules/ForbidEvalExpressions/ForbidEvalExpressionsRule.php index 11e6837..76fcb04 100644 --- a/src/Rules/ForbidEvalExpressions/ForbidEvalExpressionsRule.php +++ b/src/Rules/ForbidEvalExpressions/ForbidEvalExpressionsRule.php @@ -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 { diff --git a/src/Rules/ForbidExitExpressions/ForbidExitExpressionsRule.php b/src/Rules/ForbidExitExpressions/ForbidExitExpressionsRule.php index 95a9a26..49c977f 100644 --- a/src/Rules/ForbidExitExpressions/ForbidExitExpressionsRule.php +++ b/src/Rules/ForbidExitExpressions/ForbidExitExpressionsRule.php @@ -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 { diff --git a/src/Rules/ForbidGotoStatements/ForbidGotoStatementsRule.php b/src/Rules/ForbidGotoStatements/ForbidGotoStatementsRule.php index a0457dc..4840327 100644 --- a/src/Rules/ForbidGotoStatements/ForbidGotoStatementsRule.php +++ b/src/Rules/ForbidGotoStatements/ForbidGotoStatementsRule.php @@ -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 { diff --git a/src/Rules/ForbidPestPhpOnly/ForbidPestPhpOnlyRule.php b/src/Rules/ForbidPestPhpOnly/ForbidPestPhpOnlyRule.php index 1b0e339..80cd2c9 100644 --- a/src/Rules/ForbidPestPhpOnly/ForbidPestPhpOnlyRule.php +++ b/src/Rules/ForbidPestPhpOnly/ForbidPestPhpOnlyRule.php @@ -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', diff --git a/src/Rules/LongClassName/LongClassNameRule.php b/src/Rules/LongClassName/LongClassNameRule.php index c689c01..37e6ce4 100644 --- a/src/Rules/LongClassName/LongClassNameRule.php +++ b/src/Rules/LongClassName/LongClassNameRule.php @@ -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, diff --git a/src/Rules/LongVariable/LongVariableRule.php b/src/Rules/LongVariable/LongVariableRule.php index e05ff47..c63427b 100644 --- a/src/Rules/LongVariable/LongVariableRule.php +++ b/src/Rules/LongVariable/LongVariableRule.php @@ -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 Track variables processed in special contexts by name and line */ protected array $specialContextVariables = []; diff --git a/src/Rules/MissingClosureParameterTypehint/MissingClosureParameterTypehintRule.php b/src/Rules/MissingClosureParameterTypehint/MissingClosureParameterTypehintRule.php index 607603c..3ca3652 100644 --- a/src/Rules/MissingClosureParameterTypehint/MissingClosureParameterTypehintRule.php +++ b/src/Rules/MissingClosureParameterTypehint/MissingClosureParameterTypehintRule.php @@ -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 diff --git a/src/Rules/NumberOfChildren/NumberOfChildrenRule.php b/src/Rules/NumberOfChildren/NumberOfChildrenRule.php index d506e3b..2ef2d4b 100644 --- a/src/Rules/NumberOfChildren/NumberOfChildrenRule.php +++ b/src/Rules/NumberOfChildren/NumberOfChildrenRule.php @@ -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 diff --git a/src/Rules/PascalCaseClassName/PascalCaseClassNameRule.php b/src/Rules/PascalCaseClassName/PascalCaseClassNameRule.php index 516a3cb..4e352ae 100644 --- a/src/Rules/PascalCaseClassName/PascalCaseClassNameRule.php +++ b/src/Rules/PascalCaseClassName/PascalCaseClassNameRule.php @@ -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, diff --git a/src/Rules/ShortClassName/ShortClassNameRule.php b/src/Rules/ShortClassName/ShortClassNameRule.php index 45dedaf..2209476 100644 --- a/src/Rules/ShortClassName/ShortClassNameRule.php +++ b/src/Rules/ShortClassName/ShortClassNameRule.php @@ -19,7 +19,7 @@ */ class ShortClassNameRule implements Rule { - public const ERROR_MESSAGE_TEMPLATE = '%s name "%s" is too short (%d chars). Minimum allowed length is %d characters.'; + public const string ERROR_MESSAGE_TEMPLATE = '%s name "%s" is too short (%d chars). Minimum allowed length is %d characters.'; public function __construct( protected Config $config, diff --git a/src/Rules/ShortMethodName/ShortMethodNameRule.php b/src/Rules/ShortMethodName/ShortMethodNameRule.php index eda4945..8b3fd58 100644 --- a/src/Rules/ShortMethodName/ShortMethodNameRule.php +++ b/src/Rules/ShortMethodName/ShortMethodNameRule.php @@ -14,7 +14,7 @@ */ class ShortMethodNameRule implements Rule { - public const ERROR_MESSAGE_TEMPLATE = 'Method name "%s" is shorter than minimum length of %d characters.'; + public const string ERROR_MESSAGE_TEMPLATE = 'Method name "%s" is shorter than minimum length of %d characters.'; public function __construct( protected Config $config, diff --git a/src/Rules/ShortVariable/ShortVariableRule.php b/src/Rules/ShortVariable/ShortVariableRule.php index b7d0286..ffcc5ff 100644 --- a/src/Rules/ShortVariable/ShortVariableRule.php +++ b/src/Rules/ShortVariable/ShortVariableRule.php @@ -25,11 +25,11 @@ */ class ShortVariableRule implements Rule { - public const ERROR_MESSAGE_TEMPLATE_PARAMETER = 'Parameter name "$%s" is shorter than minimum length of %d characters.'; + public const string ERROR_MESSAGE_TEMPLATE_PARAMETER = 'Parameter name "$%s" is shorter than minimum length of %d characters.'; - public const ERROR_MESSAGE_TEMPLATE_PROPERTY = 'Property name "$%s" is shorter than minimum length of %d characters.'; + public const string ERROR_MESSAGE_TEMPLATE_PROPERTY = 'Property name "$%s" is shorter than minimum length of %d characters.'; - public const ERROR_MESSAGE_TEMPLATE_VARIABLE = 'Variable name "$%s" is shorter than minimum length of %d characters.'; + public const string ERROR_MESSAGE_TEMPLATE_VARIABLE = 'Variable name "$%s" is shorter than minimum length of %d characters.'; /** @var array Track variables processed in special contexts by name and line */ protected array $specialContextVariables = []; diff --git a/src/Rules/Superglobals/SuperglobalsRule.php b/src/Rules/Superglobals/SuperglobalsRule.php index 6bb7ef2..bbf93cf 100644 --- a/src/Rules/Superglobals/SuperglobalsRule.php +++ b/src/Rules/Superglobals/SuperglobalsRule.php @@ -14,7 +14,7 @@ */ class SuperglobalsRule implements Rule { - public const ERROR_MESSAGE_TEMPLATE = 'Superglobal "$%s" should not be used.'; + public const string ERROR_MESSAGE_TEMPLATE = 'Superglobal "$%s" should not be used.'; /** @var string[] */ protected array $superglobals = [ diff --git a/src/Rules/TraitConstantNamingConventions/TraitConstantNamingConventionsRule.php b/src/Rules/TraitConstantNamingConventions/TraitConstantNamingConventionsRule.php index a4b9210..c476b17 100644 --- a/src/Rules/TraitConstantNamingConventions/TraitConstantNamingConventionsRule.php +++ b/src/Rules/TraitConstantNamingConventions/TraitConstantNamingConventionsRule.php @@ -15,7 +15,7 @@ */ class TraitConstantNamingConventionsRule 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 From c7d3e5c3bc91d9e8a6ca636a890055d3f383fa74 Mon Sep 17 00:00:00 2001 From: Kevin Ullyott Date: Tue, 30 Dec 2025 23:29:22 -0500 Subject: [PATCH 6/6] Add regex error handling to TooManyMethodsRule Validates the ignore_pattern configuration by checking both preg_match return value and preg_last_error() to catch all potential regex errors, throwing InvalidArgumentException with a descriptive message when invalid. --- src/Rules/TooManyMethods/TooManyMethodsRule.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Rules/TooManyMethods/TooManyMethodsRule.php b/src/Rules/TooManyMethods/TooManyMethodsRule.php index 1324e4c..fa3f983 100644 --- a/src/Rules/TooManyMethods/TooManyMethodsRule.php +++ b/src/Rules/TooManyMethods/TooManyMethodsRule.php @@ -2,6 +2,7 @@ namespace Orrison\MeliorStan\Rules\TooManyMethods; +use InvalidArgumentException; use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Class_; @@ -82,7 +83,18 @@ protected function countMethods(ClassLike $node): int foreach ($node->getMethods() as $method) { $methodName = $method->name->toString(); - if (preg_match($regex, $methodName) === 0) { + $result = @preg_match($regex, $methodName); + + // Check for both false return value and error codes + if ($result === false || preg_last_error() !== PREG_NO_ERROR) { + $error = preg_last_error_msg(); + + throw new InvalidArgumentException( + sprintf('Invalid regex pattern in ignore_pattern configuration: "%s". Error: %s', $ignorePattern, $error) + ); + } + + if ($result === 0) { ++$count; } }