Add classPattern param config directive for wildcard class name matching#412
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new classPattern parameter directive to allow/disallow calls based on wildcard matching of an argument’s declared class name (via fnmatch()), filling a gap where typeString cannot represent such patterns.
Changes:
- Introduces
classPatternsupport in param directives via newParamClassPattern*implementations and config parsing inAllowedConfigFactory. - Expands config schemas/type aliases (
extension.neon,phpstan.neon) and documents the new directive (docs/allow-with-parameters.md). - Adds rule-analysis tests and fixtures for class-pattern parameter matching, plus an invalid-config test for empty
classPattern.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/Allowed/AllowedConfigFactory.php |
Parses classPattern configs and instantiates pattern-based Param implementations (with precedence over typeString). |
src/Params/ParamClassPattern.php |
Implements class-name wildcard matching for argument types using Type::getObjectClassNames() + fnmatch(). |
src/Params/ParamClassPatternExcept.php |
Negates class-pattern matching for “except/disallow” param directives. |
src/Exceptions/EmptyClassPatternInConfigException.php |
Adds a dedicated config error for empty classPattern. |
extension.neon |
Extends NEON schema to allow classPattern in relevant param directive structures. |
phpstan.neon |
Updates PHPStan type alias CallParamConfig to include classPattern. |
docs/allow-with-parameters.md |
Documents classPattern semantics, examples, and precedence over typeString. |
tests/Calls/FunctionCallsClassPatternParamsTest.php |
Adds coverage for allow/allowExcept behavior, non-object behavior, and named arguments with classPattern. |
tests/src/disallowed/functionCallsClassPatternParams.php |
Test fixture exercising allowed/disallowed cases for class patterns (incl. named arguments). |
tests/Calls/FunctionCallsInvalidTypeStringConfigTest.php |
Adds an invalid-config test ensuring empty classPattern throws. |
tests/src/Functions.php |
Adds helper functions used by the new tests. |
tests/Usages/NamespaceUsagesAllowInClassWithAttributesTest.php |
Updates expected line numbers due to added functions in tests/src/Functions.php. |
composer.json |
Excludes the new PHP-8-syntax fixture from lint-7.x. |
CLAUDE.md |
Adds guidance to consult docs when helping users with config. |
Comments suppressed due to low confidence (1)
src/Allowed/AllowedConfigFactory.php:189
CallParamConfignow allowsclassPattern, and$valuepassed intoparamFactory()originates from that config type. Even thoughparamWithoutTypeFactory()handles theclassPatterncase first, PHPStan won't infer that narrowing, soparamFactory()'s phpdoc shape for$valueshould also includeclassPattern?: string(or be relaxed) to avoid a static-analysis type mismatch.
/**
* For param directives whose config value does not resolve to a PHPStan Type implementation (e.g. classPattern).
*
* @param int|string $key
* @param int|bool|string|null|array{position:int, value?:int|bool|string, typeString?:string, classPattern?:string, name?:string} $value
* @param bool $except
* @return Param|null
* @throws InvalidConfigException
*/
private function paramWithoutTypeFactory($key, $value, bool $except): ?Param
{
if (!is_numeric($key) || !is_array($value)) {
return null;
}
$classPattern = $value['classPattern'] ?? null;
if ($classPattern === null) {
return null;
}
if ($classPattern === '') {
throw new EmptyClassPatternInConfigException();
}
$paramPosition = $value['position'];
$paramName = $value['name'] ?? null;
return $except
? new ParamClassPatternExcept($paramPosition, $paramName, $classPattern)
: new ParamClassPattern($paramPosition, $paramName, $classPattern);
}
/**
* @template T of ParamValue
* @param class-string<T> $class
* @param int|string $key
* @param int|bool|string|null|array{position:int, value?:int|bool|string, typeString?:string, name?:string} $value
* @return T
* @throws InvalidConfigException
*/
private function paramFactory(string $class, $key, $value): ParamValue
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ching `typeString` passes the value through PHPStan's type parser, which produces a concrete `Type` implementation — there is no PHPDoc type that represents "any class matching a pattern". Users who want to disallow calls based on a parameter being an instance of any class in a given namespace had no option but to enumerate every class explicitly. `classPattern` takes an fnmatch-style string and matches against the argument's declared class names directly, filling that gap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50ec125 to
529cd39
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
spaze
added a commit
that referenced
this pull request
Apr 26, 2026
…ern` Both features were added separately - wildcard support for `allowInInstanceOf`/`disallowInInstanceOf` in #399, and the `classPattern` parameter directive in #412 - but they compose into a pattern that neither covers alone: disallow a general-purpose method within a whole namespace, but only when the argument also comes from that namespace. The new example in `allow-in-instance-of.md` illustrates this with a module-scoped processor where same-module items should be handled directly rather than routed through the general processor.
spaze
added a commit
that referenced
this pull request
Apr 27, 2026
…ern` Both features were added separately - wildcard support for `allowInInstanceOf`/`disallowInInstanceOf` in #399, and the `classPattern` parameter directive in #412 - but they compose into a pattern that neither covers alone: disallow a general-purpose method within a whole namespace, but only when the argument also comes from that namespace. The new example in `allow-in-instance-of.md` illustrates this with a module-scoped processor where same-module items should be handled directly rather than routed through the general processor.
spaze
added a commit
that referenced
this pull request
Apr 27, 2026
…ern` (#417) Both features were added separately - wildcard support for `allowInInstanceOf`/`disallowInInstanceOf` in #399, and the `classPattern` parameter directive in #412 - but they compose into a pattern that neither covers alone: disallow a general-purpose method within a whole namespace, but only when the argument also comes from that namespace. The new example in `allow-in-instance-of.md` illustrates this with a module-scoped processor where same-module items should be handled directly rather than routed through the general processor.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a
classPatternconfig directive for param rules.typeStringpasses the configured value through PHPStan's type parser, which produces a concreteTypeimplementation — there is no PHPDoc type that represents "any class matching a wildcard pattern". Users who want to disallow calls based on a parameter being an instance of any class in a given namespace had no option but to enumerate every class explicitly.classPatterntakes an fnmatch-style string and matches against the argument's declared class names directly, filling that gap.It works with all param directives (
allowParamsAnywhere,allowParamsInAllowed,allowExceptParamsAnywhere,allowExceptParamsInAllowed, etc.). The matching usesfnmatch()syntax withFNM_NOESCAPE | FNM_CASEFOLDflags, consistent with the rest of the codebase. Non-object types (int, string, etc.) never match since they have no class name. The matching is done on the argument's declared class name only — it does not traverse parent classes or interfaces; usetypeStringwith a base class or interface name for that. If bothclassPatternandtypeStringare specified,classPatterntakes precedence.Internally,
classPatterndoes not go throughparamFactory(which resolves config values to PHPStanTypeimplementations) since no suchTypeexists for wildcard patterns — PHPStan'sTypeinterface is@api-do-not-implement. Instead, it goes through a newparamWithoutTypeFactorymethod, which is the designated home for any future param directives that similarly cannot resolve to a PHPStanTypeimplementation.