diff --git a/.github/workflows/coding-standard.yml b/.github/workflows/coding-standard.yml index 9ab371eb..efd16a47 100644 --- a/.github/workflows/coding-standard.yml +++ b/.github/workflows/coding-standard.yml @@ -28,7 +28,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install PHP" - uses: "shivammathur/setup-php@2.29.0" + uses: "shivammathur/setup-php@2.30.0" with: coverage: "pcov" php-version: "${{ matrix.php-version }}" diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 749dbf13..b7519e4c 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -29,7 +29,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install PHP" - uses: "shivammathur/setup-php@2.29.0" + uses: "shivammathur/setup-php@2.30.0" with: coverage: "pcov" php-version: "${{ matrix.php-version }}" diff --git a/.github/workflows/phar-creation.yml b/.github/workflows/phar-creation.yml index b1e8f56b..bc81ddf0 100644 --- a/.github/workflows/phar-creation.yml +++ b/.github/workflows/phar-creation.yml @@ -17,7 +17,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install PHP" - uses: "shivammathur/setup-php@2.29.0" + uses: "shivammathur/setup-php@2.30.0" with: coverage: "none" php-version: "8.2" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 394971f9..74e0be71 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -28,7 +28,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install PHP" - uses: "shivammathur/setup-php@2.29.0" + uses: "shivammathur/setup-php@2.30.0" with: coverage: "pcov" php-version: "${{ matrix.php-version }}" diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 66cdd3d2..9bbcd040 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -32,7 +32,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install PHP" - uses: "shivammathur/setup-php@2.29.0" + uses: "shivammathur/setup-php@2.30.0" with: coverage: "pcov" php-version: "${{ matrix.php-version }}" @@ -65,5 +65,12 @@ jobs: if: ${{ matrix.dependencies == 'locked' }} run: "composer install --no-interaction --no-progress --no-suggest" + - name: "Compile PHAR" + run: | + php -d phar.readonly=0 vendor/bin/phing phar-build + # We re-run 'composer install' here as phing runs 'composer install --no-dev'. + # Using 'install' (not 'update' with flags) here is correct as the previous update re-wrote wrote the lock file. + composer --no-interaction install --no-progress + - name: "Tests" run: "vendor/bin/phpunit --coverage-clover=clover.xml --coverage-text" diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 84b1066e..dcf06718 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -30,7 +30,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install PHP" - uses: "shivammathur/setup-php@2.29.0" + uses: "shivammathur/setup-php@2.30.0" with: coverage: "pcov" php-version: "${{ matrix.php-version }}" diff --git a/.github/workflows/require-checker.yml b/.github/workflows/require-checker.yml index 37b85fce..6dfd596e 100644 --- a/.github/workflows/require-checker.yml +++ b/.github/workflows/require-checker.yml @@ -28,7 +28,7 @@ jobs: uses: "actions/checkout@v4" - name: "Install PHP" - uses: "shivammathur/setup-php@2.29.0" + uses: "shivammathur/setup-php@2.30.0" with: coverage: "pcov" php-version: "${{ matrix.php-version }}" diff --git a/composer.lock b/composer.lock index 9ab06524..23df4497 100644 --- a/composer.lock +++ b/composer.lock @@ -2227,20 +2227,21 @@ }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -2281,9 +2282,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -2727,16 +2734,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.11", + "version": "10.1.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "78c3b7625965c2513ee96569a4dbb62601784145" + "reference": "842f72662d6b9edda84c4b6f13885fd9cd53dc63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/78c3b7625965c2513ee96569a4dbb62601784145", - "reference": "78c3b7625965c2513ee96569a4dbb62601784145", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/842f72662d6b9edda84c4b6f13885fd9cd53dc63", + "reference": "842f72662d6b9edda84c4b6f13885fd9cd53dc63", "shasum": "" }, "require": { @@ -2793,7 +2800,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.12" }, "funding": [ { @@ -2801,7 +2808,7 @@ "type": "github" } ], - "time": "2023-12-21T15:38:30+00:00" + "time": "2024-03-02T07:22:05+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3379,16 +3386,16 @@ }, { "name": "sebastian/cli-parser", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { @@ -3423,7 +3430,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -3431,7 +3439,7 @@ "type": "github" } ], - "time": "2023-02-03T06:58:15+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", @@ -3681,16 +3689,16 @@ }, { "name": "sebastian/diff", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { @@ -3698,7 +3706,7 @@ }, "require-dev": { "phpunit/phpunit": "^10.0", - "symfony/process": "^4.2 || ^5" + "symfony/process": "^6.4" }, "type": "library", "extra": { @@ -3736,7 +3744,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -3744,7 +3752,7 @@ "type": "github" } ], - "time": "2023-12-22T10:55:06+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", @@ -3812,16 +3820,16 @@ }, { "name": "sebastian/exporter", - "version": "5.1.1", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc" + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { @@ -3878,7 +3886,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" }, "funding": [ { @@ -3886,20 +3894,20 @@ "type": "github" } ], - "time": "2023-09-24T13:22:09+00:00" + "time": "2024-03-02T07:17:12+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { @@ -3933,14 +3941,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -3948,7 +3956,7 @@ "type": "github" } ], - "time": "2023-07-19T07:19:23+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", @@ -4890,16 +4898,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -4928,7 +4936,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -4936,7 +4944,7 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "vimeo/psalm", diff --git a/src/ComposerRequireChecker/Cli/Options.php b/src/ComposerRequireChecker/Cli/Options.php index db11fa43..2759c836 100644 --- a/src/ComposerRequireChecker/Cli/Options.php +++ b/src/ComposerRequireChecker/Cli/Options.php @@ -4,8 +4,13 @@ namespace ComposerRequireChecker\Cli; +use AllowDynamicProperties; +use Attribute; use ComposerRequireChecker\FileLocator\LocateComposerPackageSourceFiles; use InvalidArgumentException; +use Override; +use ReturnTypeWillChange; +use SensitiveParameter; use function array_merge; use function array_unique; @@ -36,8 +41,16 @@ class Options 'never', ]; + private const PHP_ATTRIBUTES = [ + AllowDynamicProperties::class, + Attribute::class, + Override::class, + ReturnTypeWillChange::class, + SensitiveParameter::class, + ]; + /** @var array */ - private array $symbolWhitelist = self::PHP_LANGUAGE_TYPES; + private array $symbolWhitelist; /** @var array */ private array $phpCoreExtensions = [ @@ -63,6 +76,8 @@ class Options /** @param array $options */ public function __construct(array $options = []) { + $this->symbolWhitelist = array_merge(self::PHP_LANGUAGE_TYPES, self::PHP_ATTRIBUTES); + /** @var mixed $option */ foreach ($options as $key => $option) { $methodName = 'set' . $this->getCamelCase($key); @@ -97,6 +112,7 @@ public function setSymbolWhitelist(array $symbolWhitelist): void */ $this->symbolWhitelist = array_unique(array_merge( self::PHP_LANGUAGE_TYPES, + self::PHP_ATTRIBUTES, $symbolWhitelist, )); } diff --git a/src/ComposerRequireChecker/NodeVisitor/UsedSymbolCollector.php b/src/ComposerRequireChecker/NodeVisitor/UsedSymbolCollector.php index 3ee6c8ce..5ce44120 100644 --- a/src/ComposerRequireChecker/NodeVisitor/UsedSymbolCollector.php +++ b/src/ComposerRequireChecker/NodeVisitor/UsedSymbolCollector.php @@ -47,6 +47,7 @@ public function enterNode(Node $node) $this->recordConstantFetchUsage($node); $this->recordTraitUsage($node); $this->recordPropertyTypeUsage($node); + $this->recordAttributeUsage($node); return parent::enterNode($node); } @@ -204,6 +205,15 @@ private function recordPropertyTypeUsage(Node $node): void $this->recordUsageOf($node->type); } + private function recordAttributeUsage(Node $node): void + { + if (! $node instanceof Node\Attribute) { + return; + } + + $this->recordUsageOf($node->name); + } + private function recordUsageOf(Node\Name $symbol): void { $this->recordUsageOfByString($symbol->toString()); diff --git a/test/ComposerRequireCheckerTest/BinaryTest.php b/test/ComposerRequireCheckerTest/BinaryTest.php index ad774e48..7a4e2d3a 100644 --- a/test/ComposerRequireCheckerTest/BinaryTest.php +++ b/test/ComposerRequireCheckerTest/BinaryTest.php @@ -37,7 +37,7 @@ public function testUnknownSymbols(): void $path = __DIR__ . '/../fixtures/unknownSymbols/composer.json'; exec(sprintf('%s check %s 2>&1', $this->bin, $path), $output, $return); $this->assertSame(1, $return); - $this->assertStringContainsString('The following 6 unknown symbols were found', implode("\n", $output)); + $this->assertStringContainsString('The following 7 unknown symbols were found', implode("\n", $output)); } public function testInvalidConfiguration(): void diff --git a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php index 8ca3f423..da16e054 100644 --- a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php +++ b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php @@ -70,13 +70,14 @@ public function testUnknownSymbolsFound(): void $this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode()); $display = $this->commandTester->getDisplay(); - $this->assertStringContainsString('The following 6 unknown symbols were found:', $display); + $this->assertStringContainsString('The following 7 unknown symbols were found:', $display); $this->assertStringContainsString('Doctrine\Common\Collections\ArrayCollection', $display); $this->assertStringContainsString('Example\Library\Dependency', $display); $this->assertStringContainsString('FILTER_VALIDATE_URL', $display); $this->assertStringContainsString('filter_var', $display); $this->assertStringContainsString('Foo\Bar\Baz', $display); $this->assertStringContainsString('libxml_clear_errors', $display); + $this->assertStringContainsString('Vendor\UnknownAttribute ', $display); } public function testInvalidOutputOptionValue(): void @@ -111,6 +112,7 @@ public function testUnknownSymbolsFoundJsonReport(): void 'filter_var' => ['ext-filter'], 'Foo\Bar\Baz' => [], 'libxml_clear_errors' => ['ext-libxml'], + 'Vendor\UnknownAttribute' => [], ], $actual['unknown-symbols'], ); @@ -360,4 +362,17 @@ public function testNoUnknownEnumSymbolsFound(): void $this->commandTester->getDisplay(), ); } + + public function testNoUnknownAttributeSymbolsFound(): void + { + $this->commandTester->execute([ + 'composer-json' => dirname(__DIR__, 2) . '/fixtures/noUnknownAttributeSymbols/composer.json', + ]); + + self::assertSame(Command::SUCCESS, $this->commandTester->getStatusCode()); + self::assertStringContainsString( + 'There were no unknown symbols found.', + $this->commandTester->getDisplay(), + ); + } } diff --git a/test/ComposerRequireCheckerTest/Cli/OptionsTest.php b/test/ComposerRequireCheckerTest/Cli/OptionsTest.php index ec1ddbbb..a625c4cd 100644 --- a/test/ComposerRequireCheckerTest/Cli/OptionsTest.php +++ b/test/ComposerRequireCheckerTest/Cli/OptionsTest.php @@ -46,6 +46,11 @@ public function testOptionsAcceptSymbolWhitelistAndFiltersDuplicates(): void 'object', 'mixed', 'never', + 'AllowDynamicProperties', + 'Attribute', + 'Override', + 'ReturnTypeWillChange', + 'SensitiveParameter', 'foo', 'bar', ], $options->getSymbolWhitelist()); @@ -100,6 +105,11 @@ public function testPublicSetters(): void 'object', 'mixed', 'never', + 'AllowDynamicProperties', + 'Attribute', + 'Override', + 'ReturnTypeWillChange', + 'SensitiveParameter', 'foo', 'bar', ], $options->getSymbolWhitelist()); diff --git a/test/ComposerRequireCheckerTest/PharTest.php b/test/ComposerRequireCheckerTest/PharTest.php new file mode 100644 index 00000000..1abb9b79 --- /dev/null +++ b/test/ComposerRequireCheckerTest/PharTest.php @@ -0,0 +1,96 @@ +markTestSkipped('Compiled PHAR not found'); + } + + $this->oldWorkingDirectory = getcwd(); + $this->bin = PHP_BINARY . ' ' . escapeshellarg($phar); + } + + protected function tearDown(): void + { + if ($this->oldWorkingDirectory === getcwd()) { + return; + } + + chdir($this->oldWorkingDirectory); + } + + public function testVersion(): void + { + $command = $this->bin . ' --version'; + exec($command, $output, $return); + $this->assertStringContainsString('ComposerRequireChecker', implode("\n", $output)); + $this->assertSame(0, $return); + } + + public function testSuccess(): void + { + exec($this->bin, $output, $return); + $this->assertSame(0, $return); + } + + public function testUnknownSymbols(): void + { + $path = __DIR__ . '/../fixtures/unknownSymbols/composer.json'; + exec(sprintf('%s check %s 2>&1', $this->bin, $path), $output, $return); + $this->assertStringContainsString('The following 7 unknown symbols were found', implode("\n", $output)); + $this->assertNotEquals(0, $return); + } + + public function testInvalidConfiguration(): void + { + $path = __DIR__ . '/../fixtures/validJson.json'; + exec(sprintf('%s check %s 2>&1', $this->bin, $path), $output, $return); + $this->assertStringContainsString('dependencies have not been installed', implode("\n", $output)); + $this->assertNotEquals(0, $return); + } + + public function testDefaultConfiguration(): void + { + chdir(__DIR__ . '/../fixtures/defaultConfigPath'); + exec(sprintf('%s check 2>&1', $this->bin), $output, $return); + $output = implode("\n", $output); + + $this->assertMatchesRegularExpression('/The following [12] unknown symbols were found/', $output); + $this->assertMatchesRegularExpression('/Composer\\\\InstalledVersions/', $output); + $this->assertNotEquals(0, $return); + } + + public function testMissingCustomConfiguration(): void + { + chdir(__DIR__ . '/../fixtures/noUnknownComposerSymbol'); + exec(sprintf('%s check --config-file=%s 2>&1', $this->bin, 'no-such-file'), $output, $return); + + $this->assertStringContainsString('Configuration file [no-such-file] does not exist.', implode("\n", $output)); + $this->assertNotEquals(0, $return); + } +} diff --git a/test/fixtures/noUnknownAttributeSymbols/composer.json b/test/fixtures/noUnknownAttributeSymbols/composer.json new file mode 100644 index 00000000..65aeb1d4 --- /dev/null +++ b/test/fixtures/noUnknownAttributeSymbols/composer.json @@ -0,0 +1,11 @@ +{ + "name": "example/library", + "require": { + "php": "8.1.*" + }, + "autoload": { + "psr-4": { + "Example\\Library\\": "src/" + } + } +} diff --git a/test/fixtures/noUnknownAttributeSymbols/src/SomeClass.php b/test/fixtures/noUnknownAttributeSymbols/src/SomeClass.php new file mode 100644 index 00000000..54c42500 --- /dev/null +++ b/test/fixtures/noUnknownAttributeSymbols/src/SomeClass.php @@ -0,0 +1,22 @@ +