Skip to content

Add classPattern param config directive for wildcard class name matching#412

Merged
spaze merged 1 commit into
mainfrom
spaze/classpattern-param-directive
Apr 26, 2026
Merged

Add classPattern param config directive for wildcard class name matching#412
spaze merged 1 commit into
mainfrom
spaze/classpattern-param-directive

Conversation

@spaze
Copy link
Copy Markdown
Owner

@spaze spaze commented Apr 26, 2026

Adds a classPattern config directive for param rules. typeString passes the configured value through PHPStan's type parser, which produces a concrete Type implementation — 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. classPattern takes 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 uses fnmatch() syntax with FNM_NOESCAPE | FNM_CASEFOLD flags, 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; use typeString with a base class or interface name for that. If both classPattern and typeString are specified, classPattern takes precedence.

Internally, classPattern does not go through paramFactory (which resolves config values to PHPStan Type implementations) since no such Type exists for wildcard patterns — PHPStan's Type interface is @api-do-not-implement. Instead, it goes through a new paramWithoutTypeFactory method, which is the designated home for any future param directives that similarly cannot resolve to a PHPStan Type implementation.

@spaze spaze self-assigned this Apr 26, 2026
Copilot AI review requested due to automatic review settings April 26, 2026 02:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 classPattern support in param directives via new ParamClassPattern* implementations and config parsing in AllowedConfigFactory.
  • 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

  • CallParamConfig now allows classPattern, and $value passed into paramFactory() originates from that config type. Even though paramWithoutTypeFactory() handles the classPattern case first, PHPStan won't infer that narrowing, so paramFactory()'s phpdoc shape for $value should also include classPattern?: 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.

Comment thread src/Allowed/AllowedConfigFactory.php
Comment thread src/Allowed/AllowedConfigFactory.php
…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>
@spaze spaze force-pushed the spaze/classpattern-param-directive branch from 50ec125 to 529cd39 Compare April 26, 2026 03:07
@spaze spaze requested a review from Copilot April 26, 2026 03:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 spaze merged commit f5eb3f0 into main Apr 26, 2026
154 checks passed
@spaze spaze deleted the spaze/classpattern-param-directive branch April 26, 2026 03:26
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.
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