Config::get() merges the user's typephp.php over the defaults with
array_replace_recursive() (src/Internal/Config.php:212), which merges lists by index. A
user config therefore cannot replace include/exclude; it only overwrites the first N entries.
Repro (v0.6.0) — typephp.php:
return ['include' => ['src/**'], 'exclude' => ['vendor/**', 'tests/**', 'var/**']];
require 'vendor/autoload.php';
var_export(TypePHP\Internal\Config::get()['include']);
// ['src/**', 'app/**', 'internals/**', 'tests/**'] <- app/**, internals/**, tests/** leaked in
var_export(TypePHP\Internal\Config::get()['exclude']);
// ['vendor/**', 'tests/**', 'var/**', 'cache/**'] <- storage/** silently dropped
tests/** then sits in both lists, and whether files under it are transformed comes down to
PathMatcher::isPathIncluded()'s $longestIncludeMatch > $longestExcludeMatch tie-break on equal
glob lengths (8 > 8 is false). Lengthening either glob flips the behaviour.
Impact. include/exclude do not mean what the config file says. Excluding a directory can
depend on the character count of the pattern used.
Suggested fix. Replace include, exclude, extensions and stubs wholesale when the user
config declares them, rather than recursively merging the lists.
Config::get()merges the user'stypephp.phpover the defaults witharray_replace_recursive()(src/Internal/Config.php:212), which merges lists by index. Auser config therefore cannot replace
include/exclude; it only overwrites the first N entries.Repro (v0.6.0) —
typephp.php:tests/**then sits in both lists, and whether files under it are transformed comes down toPathMatcher::isPathIncluded()'s$longestIncludeMatch > $longestExcludeMatchtie-break on equalglob lengths (
8 > 8is false). Lengthening either glob flips the behaviour.Impact.
include/excludedo not mean what the config file says. Excluding a directory candepend on the character count of the pattern used.
Suggested fix. Replace
include,exclude,extensionsandstubswholesale when the userconfig declares them, rather than recursively merging the lists.