├── .gitignore ├── bin ├── lines └── lines.php ├── .gitattributes ├── scoper.php ├── .editorconfig ├── src ├── Exception │ └── ShouldNotHappenException.php ├── Contract │ └── OutputFormatterInterface.php ├── Helpers │ ├── NumberFormat.php │ └── PrivatesAccessor.php ├── FeatureCounter │ ├── Enum │ │ └── PhpVersion.php │ ├── NodeVisitor │ │ └── FeatureCollectorNodeVisitor.php │ ├── ValueObject │ │ ├── PhpFeature.php │ │ └── FeatureCollector.php │ ├── ResultPrinter.php │ └── Analyzer │ │ └── FeatureCounterAnalyzer.php ├── Console │ ├── ViewRenderer.php │ └── OutputFormatter │ │ ├── JsonOutputFormatter.php │ │ └── TextOutputFormatter.php ├── ValueObject │ ├── TableRow.php │ └── TableView.php ├── Finder │ ├── ProjectFilesFinder.php │ └── PhpFilesFinder.php ├── Command │ ├── FeaturesCommand.php │ └── MeasureCommand.php ├── Analyser.php ├── DependencyInjection │ └── ContainerFactory.php ├── NodeVisitor │ └── StructureNodeVisitor.php └── Measurements.php ├── phpstan.neon ├── ecs.php ├── rector.php ├── LICENSE ├── prefix-code.sh ├── views └── table.php ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor 3 | 4 | /.phpunit.cache 5 | 6 | 7 | php-scoper.phar 8 | -------------------------------------------------------------------------------- /bin/lines: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 'Lines' . date('Ym'), 8 | ]; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /src/Exception/ShouldNotHappenException.php: -------------------------------------------------------------------------------- 1 | withPaths([ 9 | __DIR__ . '/bin', 10 | __DIR__ . '/src', 11 | __DIR__ . '/tests', 12 | ]) 13 | ->withSkip([ 14 | '*/Fixture/*' 15 | ]) 16 | ->withPreparedSets(psr12: true, common: true, strict: true, symplify: true); 17 | -------------------------------------------------------------------------------- /src/Helpers/NumberFormat.php: -------------------------------------------------------------------------------- 1 | withPaths([__DIR__ . '/bin', __DIR__ . '/src', __DIR__ . '/tests']) 9 | ->withPreparedSets( 10 | codeQuality: true, 11 | codingStyle: true, 12 | naming: true, 13 | privatization: true, 14 | deadCode: true, 15 | typeDeclarations: true, 16 | typeDeclarationDocblocks: true, 17 | earlyReturn: true, 18 | phpunitCodeQuality: true, 19 | ) 20 | ->withPhpSets() 21 | ->withImportNames(removeUnusedImports: true) 22 | ->withSkip([ 23 | '*/Fixture/*', 24 | ]); 25 | -------------------------------------------------------------------------------- /src/Console/ViewRenderer.php: -------------------------------------------------------------------------------- 1 | getFileContents($tableView); 15 | 16 | render($viewContent); 17 | } 18 | 19 | private function getFileContents(TableView $tableView): string 20 | { 21 | ob_start(); 22 | require $tableView->getTemplateFilePath(); 23 | $viewContent = (string) ob_get_contents(); 24 | ob_end_clean(); 25 | 26 | return $viewContent; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bin/lines.php: -------------------------------------------------------------------------------- 1 | create(); 21 | 22 | $application = $container->make(Application::class); 23 | exit($application->run()); 24 | -------------------------------------------------------------------------------- /src/ValueObject/TableRow.php: -------------------------------------------------------------------------------- 1 | name; 23 | } 24 | 25 | public function getCount(): string 26 | { 27 | return $this->count; 28 | } 29 | 30 | public function getPercent(): ?string 31 | { 32 | return $this->percent; 33 | } 34 | 35 | public function isChild(): bool 36 | { 37 | return $this->isChild; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Finder/ProjectFilesFinder.php: -------------------------------------------------------------------------------- 1 | name('*.php') 19 | ->in($projectDirectory) 20 | ->notPath('vendor') 21 | ->notPath('stubs') 22 | ->notPath('bin') 23 | ->notPath('migrations') 24 | ->notPath('data-fixtures') 25 | ->notPath('build'); 26 | 27 | /** @var SplFileInfo[] $fileInfos */ 28 | $fileInfos = iterator_to_array($finder->getIterator()); 29 | 30 | return $fileInfos; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/FeatureCounter/NodeVisitor/FeatureCollectorNodeVisitor.php: -------------------------------------------------------------------------------- 1 | featureCollector->getPhpFeatures() as $phpFeature) { 21 | $callableNodeTrigger = $phpFeature->getNodeTrigger(); 22 | if ($callableNodeTrigger($node)) { 23 | $phpFeature->increaseCount(); 24 | } 25 | } 26 | 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/FeatureCounter/ValueObject/PhpFeature.php: -------------------------------------------------------------------------------- 1 | phpVersion; 24 | } 25 | 26 | public function getName(): string 27 | { 28 | return $this->name; 29 | } 30 | 31 | public function getNodeTrigger(): callable 32 | { 33 | return $this->nodeTrigger; 34 | } 35 | 36 | public function increaseCount(): void 37 | { 38 | $this->count++; 39 | } 40 | 41 | public function getCount(): int 42 | { 43 | return $this->count; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Helpers/PrivatesAccessor.php: -------------------------------------------------------------------------------- 1 | getValue($object); 26 | } 27 | 28 | private static function setPrivateProperty(object $object, string $propertyName, mixed $value): void 29 | { 30 | $reflectionProperty = new ReflectionProperty($object, $propertyName); 31 | $reflectionProperty->setValue($object, $value); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | --------------- 3 | 4 | Copyright (c) 2025-present Tomas Votruba (https://tomasvotruba.com) 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /src/ValueObject/TableView.php: -------------------------------------------------------------------------------- 1 | title; 29 | } 30 | 31 | public function getLabel(): string 32 | { 33 | return $this->label; 34 | } 35 | 36 | public function isShouldIncludeRelative(): bool 37 | { 38 | return $this->shouldIncludeRelative; 39 | } 40 | 41 | /** 42 | * @return TableRow[] 43 | */ 44 | public function getRows(): array 45 | { 46 | return $this->tableRows; 47 | } 48 | 49 | public function getTemplateFilePath(): string 50 | { 51 | return __DIR__ . '/../../views/table.php'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /prefix-code.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # inspired from https://github.com/rectorphp/rector/blob/main/build/build-rector-scoped.sh 4 | 5 | # see https://stackoverflow.com/questions/66644233/how-to-propagate-colors-from-bash-script-to-github-action?noredirect=1#comment117811853_66644233 6 | export TERM=xterm-color 7 | 8 | # show errors 9 | set -e 10 | 11 | # script fails if trying to access to an undefined variable 12 | set -u 13 | 14 | 15 | # functions 16 | note() 17 | { 18 | MESSAGE=$1; 19 | printf "\n"; 20 | echo "\033[0;33m[NOTE] $MESSAGE\033[0m"; 21 | } 22 | 23 | # --------------------------- 24 | 25 | # 2. scope it 26 | note "Downloading php-scoper 0.18.3" 27 | wget https://github.com/humbug/php-scoper/releases/download/0.18.3/php-scoper.phar -N --no-verbose 28 | 29 | 30 | note "Running php-scoper" 31 | 32 | # Work around possible PHP memory limits 33 | php -d memory_limit=-1 php-scoper.phar add-prefix bin src vendor composer.json --config scoper.php --force --ansi --output-dir scoped-code 34 | 35 | # the output code is in "/scoped-code", lets move it up 36 | # the local directories have to be empty to move easily 37 | rm -r bin src vendor composer.json 38 | mv scoped-code/* . 39 | 40 | note "Dumping Composer Autoload" 41 | composer dump-autoload --ansi --classmap-authoritative --no-dev 42 | 43 | # make bin runnable without "php" 44 | chmod 777 "bin/lines" 45 | chmod 777 "bin/lines.php" 46 | 47 | note "Finished" 48 | -------------------------------------------------------------------------------- /src/FeatureCounter/ResultPrinter.php: -------------------------------------------------------------------------------- 1 | symfonyStyle->newLine(2); 21 | 22 | $rows = []; 23 | 24 | $previousPhpVersion = null; 25 | 26 | foreach ($featureCollector->getPhpFeatures() as $phpFeature) { 27 | $changedPhpVersion = $previousPhpVersion !== null && $previousPhpVersion !== $phpFeature->getPhpVersion(); 28 | if ($changedPhpVersion) { 29 | // add empty breakline 30 | $rows[] = new TableSeparator(); 31 | } 32 | 33 | $rows[] = [ 34 | '' . $phpFeature->getPhpVersion() . '', 35 | str_pad($phpFeature->getName(), 45, ' ', STR_PAD_RIGHT), 36 | str_pad(number_format($phpFeature->getCount(), 0, ',', ' '), 10, ' ', STR_PAD_LEFT)]; 37 | 38 | $previousPhpVersion = $phpFeature->getPhpVersion(); 39 | 40 | } 41 | 42 | $this->symfonyStyle->table(['PHP version', 'Feature count'], $rows); 43 | 44 | $this->symfonyStyle->newLine(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /views/table.php: -------------------------------------------------------------------------------- 1 | 8 |
9 |
10 | 11 | getTitle(); ?> 12 | 13 | 14 | getLabel(); ?> 15 | isShouldIncludeRelative()) { ?> 16 | / 17 | Relative 18 | 20 | 21 |
22 | getRows() as $tableRow) { ?> 23 |
24 | 25 | getName(); ?> 26 | 27 | 28 | getCount(); ?> 29 | getPercent()) { ?> 30 | 31 | / 32 | 33 | getPercent(); ?> 34 | 35 | 36 | 38 |
39 | 40 | 42 |
43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tomasvotruba/lines", 3 | "description": "Measuring the size of PHP project and its PHP features", 4 | "license": "MIT", 5 | "bin": [ 6 | "bin/lines", 7 | "bin/lines.php" 8 | ], 9 | "require": { 10 | "php": "^8.3", 11 | "symfony/console": "^6.4", 12 | "symfony/finder": "^6.4", 13 | "webmozart/assert": "^1.12", 14 | "nikic/php-parser": "^5.7", 15 | "sebastian/lines-of-code": "^4.0", 16 | "nunomaduro/termwind": "^1.17|^2.3", 17 | "entropy/entropy": "dev-main" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^12.5", 21 | "rector/rector": "^2.2", 22 | "phpstan/phpstan": "^2.1", 23 | "phpecs/phpecs": "^2.2", 24 | "tracy/tracy": "^2.11", 25 | "tomasvotruba/class-leak": "^2.1", 26 | "phpstan/extension-installer": "^1.4", 27 | "symplify/phpstan-rules": "^14.9", 28 | "rector/type-perfect": "^2.1", 29 | "symplify/phpstan-extensions": "^12.0", 30 | "tomasvotruba/unused-public": "^2.1", 31 | "rector/jack": "^0.4.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "TomasVotruba\\Lines\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "TomasVotruba\\Lines\\Tests\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "check-cs": "vendor/bin/ecs check --ansi", 45 | "fix-cs": "vendor/bin/ecs check --fix --ansi", 46 | "phpstan": "vendor/bin/phpstan analyze --ansi", 47 | "rector": "vendor/bin/rector --dry-run --ansi" 48 | }, 49 | "replace": { 50 | "symfony/polyfill-ctype": "*", 51 | "symfony/polyfill-intl-grapheme": "*" 52 | }, 53 | "config": { 54 | "platform-check": false, 55 | "allow-plugins": { 56 | "phpstan/extension-installer": true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/FeatureCounter/Analyzer/FeatureCounterAnalyzer.php: -------------------------------------------------------------------------------- 1 | parser = $parserFactory->createForNewestSupportedVersion(); 29 | } 30 | 31 | /** 32 | * @param SplFileInfo[] $fileInfos 33 | */ 34 | public function analyze(array $fileInfos): FeatureCollector 35 | { 36 | $progressBar = new ProgressBar(new ConsoleOutput()); 37 | $progressBar->start(count($fileInfos)); 38 | 39 | $featureCollectorNodeVisitor = new FeatureCollectorNodeVisitor($this->featureCollector); 40 | $nodeTraverser = new NodeTraverser($featureCollectorNodeVisitor); 41 | 42 | foreach ($fileInfos as $fileInfo) { 43 | $stmts = $this->parser->parse($fileInfo->getContents()); 44 | if ($stmts === null) { 45 | throw new ShouldNotHappenException(sprintf( 46 | 'Parsing of file "%s" resulted in null statements.', 47 | $fileInfo->getRealPath() 48 | )); 49 | } 50 | 51 | $nodeTraverser->traverse($stmts); 52 | 53 | $progressBar->advance(); 54 | } 55 | 56 | $progressBar->finish(); 57 | 58 | return $this->featureCollector; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Command/FeaturesCommand.php: -------------------------------------------------------------------------------- 1 | setName('features'); 32 | $this->setDescription('Count used PHP features in the project'); 33 | 34 | $this->addArgument('project-directory', InputArgument::OPTIONAL, 'Project directory to analyze', [getcwd()]); 35 | $this->addOption('json', null, InputOption::VALUE_NONE, 'Output in JSON format'); 36 | } 37 | 38 | /** 39 | * @return self::FAILURE|self::SUCCESS 40 | */ 41 | protected function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $projectDirectory = $input->getArgument('project-directory'); 44 | Assert::string($projectDirectory); 45 | Assert::directory($projectDirectory); 46 | 47 | $input->getOption('json'); 48 | 49 | // find project PHP files 50 | $fileInfos = $this->projectFilesFinder->find($projectDirectory); 51 | $featureCollector = $this->featureCounterAnalyzer->analyze($fileInfos); 52 | 53 | $this->symfonyStyle->newLine(); 54 | 55 | $this->resultPrinter->print($featureCollector); 56 | 57 | return Command::SUCCESS; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Finder/PhpFilesFinder.php: -------------------------------------------------------------------------------- 1 | files() 41 | ->in($directories) 42 | ->sortByName() 43 | ->name('*.php') 44 | ->notPath('tomasvotruba/lines') 45 | // fix exclude to handle directories 46 | ->filter(function (SplFileInfo $fileInfo) use ($excludes): bool { 47 | foreach ($excludes as $exclude) { 48 | if (str_contains($fileInfo->getRealPath(), $exclude)) { 49 | return \false; 50 | } 51 | } 52 | 53 | return true; 54 | }); 55 | 56 | if ($allowVendor === false) { 57 | // skip vendor directory, as we often need the full source code 58 | $phpFilesFinder->notPath('vendor'); 59 | } 60 | 61 | // symfony cache dir 62 | $phpFilesFinder->notPath('var'); 63 | 64 | return $this->resolveRealPaths($phpFilesFinder); 65 | } 66 | 67 | /** 68 | * @return string[] 69 | */ 70 | private function resolveRealPaths(Finder $finder): array 71 | { 72 | $realFilePaths = []; 73 | 74 | foreach ($finder->getIterator() as $fileInfo) { 75 | $realFilePaths[] = $fileInfo->getRealPath(); 76 | } 77 | 78 | Assert::allString($realFilePaths); 79 | 80 | return $realFilePaths; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Analyser.php: -------------------------------------------------------------------------------- 1 | measureFile($measurements, $filePath); 37 | 38 | if (is_callable($progressBarClosure)) { 39 | $progressBarClosure(); 40 | } 41 | } 42 | 43 | return $measurements; 44 | } 45 | 46 | private function measureFile(Measurements $measurements, string $filePath): void 47 | { 48 | Assert::fileExists($filePath); 49 | 50 | $fileContents = file_get_contents($filePath); 51 | Assert::string($fileContents); 52 | 53 | try { 54 | // avoid stop on invalid file contents 55 | $stmts = $this->parser->parse($fileContents); 56 | } catch (Throwable) { 57 | return; 58 | } 59 | 60 | if (! is_array($stmts)) { 61 | return; 62 | } 63 | 64 | $measurements->addFile($filePath); 65 | 66 | // measure structure 67 | $nodeTraverser = new NodeTraverser(); 68 | $nodeTraverser->addVisitor(new StructureNodeVisitor($measurements)); 69 | $nodeTraverser->traverse($stmts); 70 | 71 | // measure lines of code 72 | $initLinesOfCode = $this->resolveInitLinesOfCode($fileContents); 73 | $linesOfCode = $this->counter->countInAbstractSyntaxTree($initLinesOfCode, $stmts); 74 | 75 | $measurements->incrementLines($linesOfCode->linesOfCode()); 76 | $measurements->incrementCommentLines($linesOfCode->commentLinesOfCode()); 77 | } 78 | 79 | /** 80 | * @return int<0, max> 81 | */ 82 | private function resolveInitLinesOfCode(string $fileContents): int 83 | { 84 | $linesOfCode = substr_count($fileContents, "\n"); 85 | if ($linesOfCode === 0 && $fileContents !== '') { 86 | return 1; 87 | } 88 | 89 | return $linesOfCode; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Console/OutputFormatter/JsonOutputFormatter.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'directories' => $measurements->getDirectoryCount(), 18 | 'files' => $measurements->getFileCount(), 19 | ], 20 | 21 | 'lines_of_code' => [ 22 | 'code' => $measurements->getNonCommentLines(), 23 | 'code_relative' => $measurements->getNonCommentLinesRelative(), 24 | 'comments' => $measurements->getCommentLines(), 25 | 'comments_relative' => $measurements->getCommentLinesRelative(), 26 | 'total' => $measurements->getLines(), 27 | ], 28 | ]; 29 | 30 | if ($isShort === false) { 31 | $arrayData['structure'] = [ 32 | 'namespaces' => $measurements->getNamespaceCount(), 33 | 'classes' => $measurements->getClassCount(), 34 | 'class_methods' => $measurements->getMethodCount(), 35 | 'class_constants' => $measurements->getClassConstantCount(), 36 | 'interfaces' => $measurements->getInterfaceCount(), 37 | 'traits' => $measurements->getTraitCount(), 38 | 'enums' => $measurements->getEnumCount(), 39 | 'functions' => $measurements->getFunctionCount(), 40 | 'closures' => $measurements->getClosureCount(), 41 | 'global_constants' => $measurements->getGlobalConstantCount(), 42 | ]; 43 | 44 | $arrayData['methods_access'] = [ 45 | 'non_static' => $measurements->getNonStaticMethods(), 46 | 'non_static_relative' => $measurements->getNonStaticMethodsRelative(), 47 | 'static' => $measurements->getStaticMethods(), 48 | 'static_relative' => $measurements->getStaticMethodsRelative(), 49 | ]; 50 | 51 | $arrayData['methods_visibility'] = [ 52 | 'public' => $measurements->getPublicMethods(), 53 | 'public_relative' => $measurements->getPublicMethodsRelative(), 54 | 'protected' => $measurements->getProtectedMethods(), 55 | 'protected_relative' => $measurements->getProtectedMethodsRelative(), 56 | 'private' => $measurements->getPrivateMethods(), 57 | 'private_relative' => $measurements->getPrivateMethodsRelative(), 58 | ]; 59 | } 60 | 61 | if ($showLongestFiles) { 62 | $arrayData['longest_files'] = $measurements->getLongestFiles(); 63 | } 64 | 65 | $jsonString = json_encode($arrayData, JSON_PRETTY_PRINT); 66 | Assert::string($jsonString); 67 | 68 | echo $jsonString . PHP_EOL; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/DependencyInjection/ContainerFactory.php: -------------------------------------------------------------------------------- 1 | emulateTokensOfOlderPHP(); 22 | 23 | $container = new Container(); 24 | $container->autodiscover(__DIR__ . '/../Command'); 25 | 26 | // console 27 | $consoleVerbosity = defined( 28 | 'PHPUNIT_COMPOSER_INSTALL' 29 | ) ? ConsoleOutput::VERBOSITY_QUIET : ConsoleOutput::VERBOSITY_NORMAL; 30 | 31 | $container->service( 32 | SymfonyStyle::class, 33 | static fn (): SymfonyStyle => new SymfonyStyle(new ArrayInput([]), new ConsoleOutput($consoleVerbosity)) 34 | ); 35 | 36 | $container->service(Application::class, function (Container $container): Application { 37 | $application = new Application(); 38 | 39 | $commands = $container->findByContract(Command::class); 40 | $application->addCommands($commands); 41 | 42 | // remove basic command to make output clear 43 | $this->cleanupDefaultCommands($application); 44 | 45 | return $application; 46 | }); 47 | 48 | $container->service(Parser::class, static function (): Parser { 49 | $phpParserFactory = new ParserFactory(); 50 | return $phpParserFactory->createForHostVersion(); 51 | }); 52 | 53 | return $container; 54 | } 55 | 56 | private function cleanupDefaultCommands(Application $application): void 57 | { 58 | $application->get('help') 59 | ->setHidden(true); 60 | 61 | PrivatesAccessor::propertyClosure($application, 'commands', static function (array $commands): array { 62 | // remove default commands, as not needed here 63 | unset($commands['completion']); 64 | 65 | return $commands; 66 | }); 67 | } 68 | 69 | private function emulateTokensOfOlderPHP(): void 70 | { 71 | // define fallback constants for PHP 8.0 tokens in case of e.g. PHP 7.2 run 72 | if (! defined('T_MATCH')) { 73 | define('T_MATCH', 5000); 74 | } 75 | 76 | if (! defined('T_READONLY')) { 77 | define('T_READONLY', 5010); 78 | } 79 | 80 | if (! defined('T_ENUM')) { 81 | define('T_ENUM', 5015); 82 | } 83 | 84 | if (! defined('T_COALESCE_EQUAL')) { 85 | define('T_COALESCE_EQUAL', 5020); 86 | } 87 | 88 | if (! defined('T_FN')) { 89 | define('T_FN', 5030); 90 | } 91 | 92 | if (! defined('T_BAD_CHARACTER')) { 93 | define('T_BAD_CHARACTER', 5040); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Command/MeasureCommand.php: -------------------------------------------------------------------------------- 1 | setName('measure'); 33 | $this->setDescription('Measure lines of code in given path(s)'); 34 | 35 | $this->addArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Path to analyze', [getcwd()]); 36 | $this->addOption( 37 | 'exclude', 38 | null, 39 | InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 40 | 'Paths to exclude', 41 | [] 42 | ); 43 | 44 | $this->addOption('json', null, InputOption::VALUE_NONE, 'Output in JSON format'); 45 | $this->addOption('short', null, InputOption::VALUE_NONE, 'Print short metrics only'); 46 | $this->addOption('allow-vendor', null, InputOption::VALUE_NONE, 'Allow /vendor directory to be scanned'); 47 | $this->addOption('longest', null, InputOption::VALUE_NONE, 'Show top 10 longest files'); 48 | } 49 | 50 | /** 51 | * @return self::FAILURE|self::SUCCESS 52 | */ 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $paths = (array) $input->getArgument('paths'); 56 | $excludes = (array) $input->getOption('exclude'); 57 | $isJson = (bool) $input->getOption('json'); 58 | $isShort = (bool) $input->getOption('short'); 59 | $allowVendor = (bool) $input->getOption('allow-vendor'); 60 | $showLongestFiles = (bool) $input->getOption('longest'); 61 | 62 | $filePaths = $this->phpFilesFinder->findInDirectories($paths, $excludes, $allowVendor); 63 | if ($filePaths === []) { 64 | $output->writeln('No files found to scan'); 65 | return Command::FAILURE; 66 | } 67 | 68 | $progressBarClosure = $this->createProgressBarClosure($isJson, $filePaths); 69 | $measurements = $this->analyser->measureFiles($filePaths, $progressBarClosure); 70 | 71 | // print results 72 | if ($isJson) { 73 | $this->jsonOutputFormatter->printMeasurement($measurements, $isShort, $showLongestFiles); 74 | } else { 75 | $this->textOutputFormatter->printMeasurement($measurements, $isShort, $showLongestFiles); 76 | } 77 | 78 | return Command::SUCCESS; 79 | } 80 | 81 | /** 82 | * @param string[] $filePaths 83 | */ 84 | private function createProgressBarClosure(bool $isJson, array $filePaths): ?callable 85 | { 86 | if ($isJson) { 87 | return null; 88 | } 89 | 90 | $progressBar = $this->symfonyStyle->createProgressBar(count($filePaths)); 91 | $progressBar->start(); 92 | 93 | return static function () use ($progressBar): void { 94 | $progressBar->advance(); 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/NodeVisitor/StructureNodeVisitor.php: -------------------------------------------------------------------------------- 1 | measureClassLikes($node); 33 | return $node; 34 | } 35 | 36 | if ($node instanceof ClassMethod) { 37 | $this->measureClassMethod($node); 38 | return $node; 39 | } 40 | 41 | if ($this->isDefineFuncCall($node)) { 42 | $this->measurements->incrementGlobalConstantCount(); 43 | return $node; 44 | } 45 | 46 | if ($node instanceof Namespace_) { 47 | if (! $node->name instanceof Name) { 48 | return null; 49 | } 50 | 51 | $namespaceName = $node->name->toString(); 52 | $this->measurements->addNamespace($namespaceName); 53 | 54 | return $node; 55 | } 56 | 57 | if ($node instanceof Function_) { 58 | $this->measurements->incrementFunctionCount(); 59 | 60 | return $node; 61 | } 62 | 63 | if ($node instanceof Closure) { 64 | $this->measurements->incrementClosureCount(); 65 | 66 | return $node; 67 | } 68 | 69 | return null; 70 | } 71 | 72 | private function measureClassLikes(ClassLike $classLike): void 73 | { 74 | if ($classLike instanceof Class_) { 75 | $constantCount = count($classLike->getConstants()); 76 | $this->measurements->incrementClassConstants($constantCount); 77 | 78 | if ($classLike->isAnonymous()) { 79 | return; 80 | } 81 | 82 | $this->measurements->incrementClassCount(); 83 | } 84 | 85 | if ($classLike instanceof Enum_) { 86 | $this->measurements->incrementEnumCount(); 87 | return; 88 | } 89 | 90 | if ($classLike instanceof Interface_) { 91 | $this->measurements->incrementInterfaceCount(); 92 | return; 93 | } 94 | 95 | if ($classLike instanceof Trait_) { 96 | $this->measurements->incrementTraitCount(); 97 | } 98 | } 99 | 100 | private function measureClassMethod(ClassMethod $classMethod): void 101 | { 102 | if ($classMethod->isPrivate()) { 103 | $this->measurements->incrementPrivateMethods(); 104 | } elseif ($classMethod->isProtected()) { 105 | $this->measurements->incrementProtectedMethods(); 106 | } elseif ($classMethod->isPublic()) { 107 | $this->measurements->incrementPublicMethods(); 108 | } 109 | 110 | if ($classMethod->isStatic()) { 111 | $this->measurements->incrementStaticMethods(); 112 | } else { 113 | $this->measurements->incrementNonStaticMethods(); 114 | } 115 | } 116 | 117 | private function isDefineFuncCall(Node $node): bool 118 | { 119 | if (! $node instanceof FuncCall) { 120 | return false; 121 | } 122 | 123 | if (! $node->name instanceof Name) { 124 | return false; 125 | } 126 | 127 | return $node->name->toLowerString() === 'define'; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Console/OutputFormatter/TextOutputFormatter.php: -------------------------------------------------------------------------------- 1 | symfonyStyle->newLine(); 27 | 28 | $this->printFilesAndDirectories($measurements); 29 | $this->printLinesOfCode($measurements); 30 | 31 | if ($isShort) { 32 | $this->symfonyStyle->newLine(); 33 | 34 | return; 35 | } 36 | 37 | $this->printStructure($measurements); 38 | $this->printMethods($measurements); 39 | 40 | if ($showLongestFiles) { 41 | $rows = []; 42 | foreach ($measurements->getLongestFiles() as $filePath => $linesCount) { 43 | $rows[] = [$filePath, $linesCount]; 44 | } 45 | 46 | $tableRows = $this->formatRows($rows); 47 | $tableView = new TableView('Longest files', 'Line count', $tableRows); 48 | $this->viewRenderer->renderTableView($tableView); 49 | } 50 | 51 | $this->symfonyStyle->newLine(); 52 | } 53 | 54 | private function printFilesAndDirectories(Measurements $measurements): void 55 | { 56 | $tableRows = $this->formatRows([ 57 | ['Directories', $measurements->getDirectoryCount()], 58 | ['Files', $measurements->getFileCount()], 59 | ]); 60 | 61 | $tableView = new TableView('Filesystem', 'Count', $tableRows); 62 | $this->viewRenderer->renderTableView($tableView); 63 | } 64 | 65 | private function printLinesOfCode(Measurements $measurements): void 66 | { 67 | $tableRows = $this->formatRows([ 68 | ['Code', $measurements->getNonCommentLines(), $measurements->getNonCommentLinesRelative()], 69 | ['Comments', $measurements->getCommentLines(), $measurements->getCommentLinesRelative()], 70 | ['Total', $measurements->getLines(), 100.0], 71 | ]); 72 | 73 | $tableView = new TableView('Lines of code', 'Count', $tableRows, true); 74 | $this->viewRenderer->renderTableView($tableView); 75 | } 76 | 77 | private function printMethods(Measurements $measurements): void 78 | { 79 | if ($measurements->getMethodCount() === 0) { 80 | return; 81 | } 82 | 83 | $tableRows = $this->formatRows([ 84 | ['Non-static', $measurements->getNonStaticMethods(), $measurements->getNonStaticMethodsRelative()], 85 | ['Static', $measurements->getStaticMethods(), $measurements->getStaticMethodsRelative()], 86 | ]); 87 | 88 | $tableView = new TableView('Method access', 'Count', $tableRows, true); 89 | $this->viewRenderer->renderTableView($tableView); 90 | 91 | $tableRows = $this->formatRows([ 92 | ['Public', $measurements->getPublicMethods(), $measurements->getPublicMethodsRelative()], 93 | ['Protected', $measurements->getProtectedMethods(), $measurements->getProtectedMethodsRelative()], 94 | ['Private', $measurements->getPrivateMethods(), $measurements->getPrivateMethodsRelative()], 95 | ]); 96 | 97 | $tableView = new TableView('Method visibility', 'Count', $tableRows, true); 98 | $this->viewRenderer->renderTableView($tableView); 99 | } 100 | 101 | private function printStructure(Measurements $measurements): void 102 | { 103 | $tableRows = $this->formatRows([ 104 | ['Namespaces', $measurements->getNamespaceCount()], 105 | ['Classes', $measurements->getClassCount()], 106 | ['* Constants', $measurements->getClassConstantCount(), null, true], 107 | ['* Methods', $measurements->getMethodCount(), null, true], 108 | ['Interfaces', $measurements->getInterfaceCount()], 109 | ['Traits', $measurements->getTraitCount()], 110 | ['Enums', $measurements->getEnumCount()], 111 | ['Functions', $measurements->getFunctionCount()], 112 | ['Closures', $measurements->getClosureCount()], 113 | ['Global constants', $measurements->getGlobalConstantCount()], 114 | ]); 115 | 116 | $tableView = new TableView('Structure', 'Count', $tableRows); 117 | $this->viewRenderer->renderTableView($tableView); 118 | } 119 | 120 | /** 121 | * @param array $rows 122 | * @return TableRow[] 123 | */ 124 | private function formatRows(array $rows): array 125 | { 126 | return array_map(static function (array $row): TableRow { 127 | Assert::notEmpty($row); 128 | 129 | $prettyNumber = NumberFormat::pretty($row[1]); 130 | 131 | return new TableRow($row[0], $prettyNumber, isset($row[2]) ? NumberFormat::percent( 132 | $row[2] 133 | ) : null, $row[3] ?? false); 134 | }, $rows); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Measurements.php: -------------------------------------------------------------------------------- 1 | 55 | */ 56 | private array $filesToSize = []; 57 | 58 | public function addFile(string $filename): void 59 | { 60 | $this->directoryNames[] = dirname($filename); 61 | 62 | $relativeFilePath = str_replace(getcwd() . '/', '', $filename); 63 | $this->filesToSize[$relativeFilePath] = substr_count((string) file_get_contents($filename), "\n") + 1; 64 | 65 | ++$this->fileCount; 66 | } 67 | 68 | public function incrementLines(int $number): void 69 | { 70 | $this->lineCount += $number; 71 | } 72 | 73 | public function incrementCommentLines(int $number): void 74 | { 75 | $this->commentLineCount += $number; 76 | } 77 | 78 | public function addNamespace(string $namespace): void 79 | { 80 | $this->namespaceNames[] = $namespace; 81 | } 82 | 83 | public function incrementInterfaceCount(): void 84 | { 85 | ++$this->interfaceCount; 86 | } 87 | 88 | public function incrementTraitCount(): void 89 | { 90 | ++$this->traitCount; 91 | } 92 | 93 | public function incrementNonStaticMethods(): void 94 | { 95 | ++$this->nonStaticMethodCount; 96 | } 97 | 98 | public function incrementStaticMethods(): void 99 | { 100 | ++$this->staticMethodCount; 101 | } 102 | 103 | public function incrementPublicMethods(): void 104 | { 105 | ++$this->publicMethodCount; 106 | } 107 | 108 | public function incrementProtectedMethods(): void 109 | { 110 | ++$this->protectedMethodCount; 111 | } 112 | 113 | public function incrementPrivateMethods(): void 114 | { 115 | ++$this->privateMethodCount; 116 | } 117 | 118 | public function incrementFunctionCount(): void 119 | { 120 | ++$this->functionCount; 121 | } 122 | 123 | public function incrementClosureCount(): void 124 | { 125 | ++$this->closureCount; 126 | } 127 | 128 | public function incrementGlobalConstantCount(): void 129 | { 130 | ++$this->globalConstantCount; 131 | } 132 | 133 | public function incrementClassConstants(int $count): void 134 | { 135 | $this->classConstantCount += $count; 136 | } 137 | 138 | public function incrementClassCount(): void 139 | { 140 | ++$this->classCount; 141 | } 142 | 143 | public function incrementEnumCount(): void 144 | { 145 | ++$this->enumCount; 146 | } 147 | 148 | public function getDirectoryCount(): int 149 | { 150 | $uniqueDirectoryNames = array_unique($this->directoryNames); 151 | return count($uniqueDirectoryNames) - 1; 152 | 153 | } 154 | 155 | public function getFileCount(): int 156 | { 157 | return $this->fileCount; 158 | } 159 | 160 | public function getLines(): int 161 | { 162 | return $this->lineCount; 163 | } 164 | 165 | public function getCommentLines(): int 166 | { 167 | return $this->commentLineCount; 168 | } 169 | 170 | public function getNonCommentLines(): int 171 | { 172 | return $this->lineCount - $this->commentLineCount; 173 | } 174 | 175 | public function getNamespaceCount(): int 176 | { 177 | $uniqueNamespaceNames = array_unique($this->namespaceNames); 178 | return count($uniqueNamespaceNames); 179 | } 180 | 181 | public function getInterfaceCount(): int 182 | { 183 | return $this->interfaceCount; 184 | } 185 | 186 | public function getTraitCount(): int 187 | { 188 | return $this->traitCount; 189 | } 190 | 191 | public function getClassCount(): int 192 | { 193 | return $this->classCount; 194 | } 195 | 196 | public function getMethodCount(): int 197 | { 198 | return $this->nonStaticMethodCount + $this->staticMethodCount; 199 | } 200 | 201 | public function getNonStaticMethods(): int 202 | { 203 | return $this->nonStaticMethodCount; 204 | } 205 | 206 | public function getStaticMethods(): int 207 | { 208 | return $this->staticMethodCount; 209 | } 210 | 211 | public function getPublicMethods(): int 212 | { 213 | return $this->publicMethodCount; 214 | } 215 | 216 | public function getProtectedMethods(): int 217 | { 218 | return $this->protectedMethodCount; 219 | } 220 | 221 | public function getPrivateMethods(): int 222 | { 223 | return $this->privateMethodCount; 224 | } 225 | 226 | public function getFunctionCount(): int 227 | { 228 | return $this->functionCount; 229 | } 230 | 231 | public function getClosureCount(): int 232 | { 233 | return $this->closureCount; 234 | } 235 | 236 | public function getGlobalConstantCount(): int 237 | { 238 | return $this->globalConstantCount; 239 | } 240 | 241 | public function getClassConstantCount(): int 242 | { 243 | return $this->classConstantCount; 244 | } 245 | 246 | public function getCommentLinesRelative(): float 247 | { 248 | if ($this->lineCount !== 0) { 249 | return $this->relative($this->commentLineCount, $this->lineCount); 250 | } 251 | 252 | return 0.0; 253 | } 254 | 255 | public function getNonCommentLinesRelative(): float 256 | { 257 | if ($this->lineCount !== 0) { 258 | return $this->relative($this->getNonCommentLines(), $this->lineCount); 259 | } 260 | 261 | return 0.0; 262 | } 263 | 264 | public function getStaticMethodsRelative(): float 265 | { 266 | if ($this->getMethodCount() > 0) { 267 | return $this->relative($this->staticMethodCount, $this->getMethodCount()); 268 | } 269 | 270 | return 0.0; 271 | } 272 | 273 | public function getNonStaticMethodsRelative(): float 274 | { 275 | if ($this->getMethodCount() > 0) { 276 | return $this->relative($this->nonStaticMethodCount, $this->getMethodCount()); 277 | } 278 | 279 | return 0.0; 280 | } 281 | 282 | public function getPublicMethodsRelative(): float 283 | { 284 | if ($this->getMethodCount() !== 0) { 285 | return $this->relative($this->publicMethodCount, $this->getMethodCount()); 286 | } 287 | 288 | return 0.0; 289 | } 290 | 291 | public function getProtectedMethodsRelative(): float 292 | { 293 | if ($this->getMethodCount() !== 0) { 294 | return $this->relative($this->protectedMethodCount, $this->getMethodCount()); 295 | } 296 | 297 | return 0.0; 298 | } 299 | 300 | public function getPrivateMethodsRelative(): float 301 | { 302 | if ($this->getMethodCount() !== 0) { 303 | return $this->relative($this->privateMethodCount, $this->getMethodCount()); 304 | } 305 | 306 | return 0.0; 307 | } 308 | 309 | public function getEnumCount(): int 310 | { 311 | return $this->enumCount; 312 | } 313 | 314 | /** 315 | * @return array 316 | */ 317 | public function getLongestFiles(): array 318 | { 319 | // longest files first 320 | arsort($this->filesToSize); 321 | 322 | // get top 10 323 | return array_slice($this->filesToSize, 0, 10); 324 | } 325 | 326 | private function relative(int $partialNumber, int $totalNumber): float 327 | { 328 | $relative = ($partialNumber / $totalNumber) * 100; 329 | return NumberFormat::singleDecimal($relative); 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lines of code and PHP Features 2 | 3 | CLI tool for quick size measure of PHP project, and real used PHP features. 4 | 5 | Zero dependencies. Runs anywhere. 6 | 7 |
8 | 9 | ## What are killer features? 10 | 11 | * install anywhere - PHP 7.2? PHPUnit 6? Symfony 3? Not a problem, this package **has zero dependencies and works on PHP 7.2+** 12 | * get quick overview of your project size - no details, no complexity, just lines of code 13 | * get easy **JSON output** for further processing 14 | * measure **used PHP features in your project** - how much PHP 8.0-features used? How many attributes? How many arrow function? How many union types? 15 | 16 |
17 | 18 | ## Install 19 | 20 | The package is scoped and downgraded to PHP 7.2. So you can install it anywhere with any set of dependencies: 21 | 22 | ```bash 23 | composer require tomasvotruba/lines --dev 24 | ``` 25 | 26 |
27 | 28 | ## 1. Measure Lines and Size 29 | 30 | ```bash 31 | vendor/bin/lines measure 32 | ``` 33 | 34 | By default, we measure the root directory. To narrow it down, provide explicit path: 35 | 36 | ```bash 37 | vendor/bin/lines measure src 38 | ``` 39 | 40 | For short output: 41 | 42 | ```bash 43 | vendor/bin/lines measure --short 44 | ``` 45 | 46 | For json output, just add `--json`: 47 | 48 | ```bash 49 | vendor/bin/lines measure --json 50 | ``` 51 | 52 | Also, you can combine them (very handy for blog posts and tweets): 53 | 54 | ```bash 55 | vendor/bin/lines measure --short --json 56 | ``` 57 | 58 |
59 | 60 | For the text output, you'll get data like these: 61 | 62 | ```bash 63 | Filesystem count 64 | Directories ......................................... 32 65 | Files .............................................. 160 66 | 67 | Lines of code count / relative 68 | Code ................................... 15 521 / 70.9 % 69 | Comments ................................ 6 372 / 29.1 % 70 | Total .................................. 21 893 / 100 % 71 | 72 | Structure count 73 | Namespaces .......................................... 32 74 | Classes ............................................ 134 75 | * Constants ........................................ 91 76 | * Methods ....................................... 1 114 77 | Interfaces .......................................... 20 78 | Traits ............................................... 4 79 | Enums ................................................ 1 80 | Functions ........................................... 36 81 | Global constants ..................................... 0 82 | 83 | Methods count / relative 84 | Non-static .............................. 1 058 / 95 % 85 | Static ..................................... 56 / 5 % 86 | 87 | Public .................................... 875 / 78.5 % 88 | Protected .................................. 90 / 8.1 % 89 | Private ................................... 149 / 13.4 % 90 | ``` 91 | 92 | Or in a json format: 93 | 94 | ```json 95 | { 96 | "filesystem": { 97 | "directories": 10, 98 | "files": 15 99 | }, 100 | "lines_of_code": { 101 | "code": 1064, 102 | "code_relative": 95.4, 103 | "comments": 51, 104 | "comments_relative": 4.6, 105 | "total": 1115 106 | }, 107 | "structure": { 108 | "namespaces": 11, 109 | "classes": 14, 110 | "class_methods": 88, 111 | "class_constants": 0, 112 | "interfaces": 1, 113 | "traits": 0, 114 | "enums": 0, 115 | "functions": 5, 116 | "global_constants": 3 117 | }, 118 | "methods_access": { 119 | "non_static": 82, 120 | "non_static_relative": 93.2, 121 | "static": 6, 122 | "static_relative": 6.8 123 | }, 124 | "methods_visibility": { 125 | "public": 70, 126 | "public_relative": 79.5, 127 | "protected": 2, 128 | "protected_relative": 2.3, 129 | "private": 16, 130 | "private_relative": 18.2 131 | } 132 | } 133 | ``` 134 | 135 |
136 | 137 | ### Longest files 138 | 139 | Are you looking for top 10 longest files? 140 | 141 | ```bash 142 | vendor/bin/lines measure --longest 143 | ``` 144 | 145 | ↓ 146 | 147 | ```bash 148 | Longest files line count 149 | src/Measurements.php ............................... 320 150 | src/Console/OutputFormatter/TextOutputFormatter.php 136 151 | src/NodeVisitor/StructureNodeVisitor.php ........... 124 152 | src/Console/Command/MeasureCommand.php .............. 98 153 | src/Analyser.php .................................... 92 154 | ``` 155 | 156 |
157 | 158 | ### Scan package in `/vendor` 159 | 160 | This tool measures *your code*, not the 3rd party libraries. It skips `/vendor` directory by default to avoid false positives. If you want to measure vendor files too, use `--allow-vendor` option: 161 | 162 | ```bash 163 | vendor/bin/lines measure vendor/rector/rector --allow-vendor 164 | ``` 165 | 166 |
167 | 168 | ## 2. PHP Feature Counter 169 | 170 | Two codebases using PHP 8.4 in `composer.json`, are not the same codebases. One has zero type param/return/property declarations, other has promoted properties. Reveal their real value by counting PHP feature they actually use. 171 | 172 | ```bash 173 | vendor/bin/lines features src 174 | ``` 175 | 176 | This command: 177 | 178 | * scans your codebase, 179 | * count PHP feature being used from which PHP version, 180 | * gives you quick overview of how modern the codebase really is 181 | 182 | 183 | ↓ 184 | 185 | ```bash 186 | ------------- ----------------------------------------------- ------------ 187 | PHP version Feature count 188 | ------------- ----------------------------------------------- ------------ 189 | 7.0 Parameter types 2 793 190 | 7.0 Return types 1 736 191 | 7.0 Strict declares 492 192 | 7.0 Space ship <=> operator 0 193 | ------------- ----------------------------------------------- ------------ 194 | 7.1 Nullable type (?type) 333 195 | 7.1 Void return type 317 196 | 7.1 Class constant visibility 557 197 | ------------- ----------------------------------------------- ------------ 198 | 7.2 Object type 14 199 | ------------- ----------------------------------------------- ------------ 200 | 7.3 Coalesce ?? operator 69 201 | ------------- ----------------------------------------------- ------------ 202 | 7.4 Typed properties 156 203 | 7.4 Arrow functions 38 204 | 7.4 Coalesce assign (??=) 0 205 | ------------- ----------------------------------------------- ------------ 206 | 8.0 Named arguments 10 207 | 8.0 Union types 147 208 | 8.0 Match expression 1 209 | 8.0 Nullsafe method call/property fetch 0 210 | 8.0 Attributes 0 211 | 8.0 Throw expression 111 212 | 8.0 Promoted properties 596 213 | ------------- ----------------------------------------------- ------------ 214 | 8.1 First-class callables 8 215 | 8.1 Readonly property 3 216 | 8.1 Intersection types 0 217 | 8.1 Enums 0 218 | ------------- ----------------------------------------------- ------------ 219 | 8.2 Readonly class 182 220 | ------------- ----------------------------------------------- ------------ 221 | 8.3 Typed class constants 0 222 | ------------- ----------------------------------------------- ------------ 223 | 8.4 Property hooks 0 224 | ------------- ----------------------------------------------- ------------ 225 | ``` 226 | 227 |
228 | 229 | That's it. Happy coding! 230 | 231 | 232 | -------------------------------------------------------------------------------- /src/FeatureCounter/ValueObject/FeatureCollector.php: -------------------------------------------------------------------------------- 1 | phpFeatures[] = new PhpFeature( 43 | PhpVersion::PHP_70, 44 | 'Parameter types', 45 | fn (Node $node): bool => $node instanceof Param && $node->type instanceof Node, 46 | ); 47 | 48 | $this->phpFeatures[] = new PhpFeature( 49 | PhpVersion::PHP_70, 50 | 'Return types', 51 | fn (Node $node): bool => $node instanceof FunctionLike && $node->getReturnType() !== null, 52 | ); 53 | 54 | $this->phpFeatures[] = new PhpFeature( 55 | PhpVersion::PHP_74, 56 | 'Typed properties', 57 | fn (Node $node): bool => $node instanceof Property && $node->type instanceof Node, 58 | ); 59 | 60 | $this->phpFeatures[] = new PhpFeature( 61 | PhpVersion::PHP_70, 62 | 'Strict declares', 63 | fn (Node $node): bool => $node instanceof Declare_, 64 | ); 65 | 66 | $this->phpFeatures[] = new PhpFeature( 67 | PhpVersion::PHP_70, 68 | 'Space ship <=> operator ', 69 | fn (Node $node): bool => $node instanceof Spaceship, 70 | ); 71 | 72 | $this->phpFeatures[] = new PhpFeature( 73 | PhpVersion::PHP_71, 74 | 'Nullable type (?type)', 75 | function (Node $node): bool { 76 | if ($node instanceof NullableType) { 77 | return true; 78 | } 79 | 80 | if (! $node instanceof UnionType) { 81 | return false; 82 | } 83 | 84 | // include here, count as nullable type 85 | return $this->isNullableUnionType($node); 86 | } 87 | ); 88 | 89 | $this->phpFeatures[] = new PhpFeature( 90 | PhpVersion::PHP_71, 91 | 'Void return type', 92 | fn (Node $node): bool => $node instanceof FunctionLike && $node->getReturnType() instanceof Identifier && $node->getReturnType() 93 | ->name === 'void', 94 | ); 95 | 96 | $this->phpFeatures[] = new PhpFeature( 97 | PhpVersion::PHP_72, 98 | 'Object type', 99 | fn (Node $node): bool => $node instanceof Identifier && $node->toString() === 'object', 100 | ); 101 | 102 | $this->phpFeatures[] = new PhpFeature( 103 | PhpVersion::PHP_73, 104 | 'Coalesce ?? operator', 105 | fn (Node $node): bool => $node instanceof \PhpParser\Node\Expr\BinaryOp\Coalesce, 106 | ); 107 | 108 | // class constant visibility 109 | $this->phpFeatures[] = new PhpFeature( 110 | PhpVersion::PHP_71, 111 | 'Class constant visibility', 112 | fn (Node $node): bool => $node instanceof ClassConst && ($node->flags & Modifiers::VISIBILITY_MASK) !== 0, 113 | ); 114 | 115 | $this->phpFeatures[] = new PhpFeature( 116 | PhpVersion::PHP_80, 117 | 'Named arguments', 118 | fn (Node $node): bool => $node instanceof Arg && $node->name instanceof Identifier, 119 | ); 120 | 121 | $this->phpFeatures[] = new PhpFeature( 122 | PhpVersion::PHP_81, 123 | 'First-class callables', 124 | fn (Node $node): bool => $node instanceof CallLike && $node->isFirstClassCallable() 125 | ); 126 | 127 | // readonly property 128 | $this->phpFeatures[] = new PhpFeature( 129 | PhpVersion::PHP_81, 130 | 'Readonly property', 131 | fn (Node $node): bool => $node instanceof Property && $node->isReadonly(), 132 | ); 133 | 134 | // readonly class 135 | $this->phpFeatures[] = new PhpFeature( 136 | PhpVersion::PHP_82, 137 | 'Readonly class', 138 | fn (Node $node): bool => $node instanceof Class_ && $node->isReadonly(), 139 | ); 140 | 141 | // typed class constants 142 | $this->phpFeatures[] = new PhpFeature( 143 | PhpVersion::PHP_83, 144 | 'Typed class constants', 145 | fn (Node $node): bool => $node instanceof ClassConst && $node->type instanceof Node, 146 | ); 147 | 148 | // arrow function 149 | $this->phpFeatures[] = new PhpFeature( 150 | PhpVersion::PHP_74, 151 | 'Arrow functions', 152 | fn (Node $node): bool => $node instanceof ArrowFunction, 153 | ); 154 | 155 | // coalesce assign (??=) 156 | $this->phpFeatures[] = new PhpFeature( 157 | PhpVersion::PHP_74, 158 | 'Coalesce assign (??=)', 159 | fn (Node $node): bool => $node instanceof Coalesce, 160 | ); 161 | 162 | // union types 163 | $this->phpFeatures[] = new PhpFeature( 164 | PhpVersion::PHP_80, 165 | 'Union types', 166 | function (Node $node): bool { 167 | if (! $node instanceof UnionType) { 168 | return false; 169 | } 170 | 171 | // skip here, count as nullable type 172 | return ! $this->isNullableUnionType($node); 173 | } 174 | ); 175 | 176 | // intersection types 177 | $this->phpFeatures[] = new PhpFeature( 178 | PhpVersion::PHP_81, 179 | 'Intersection types', 180 | fn (Node $node): bool => $node instanceof IntersectionType, 181 | ); 182 | 183 | // property hooks 184 | $this->phpFeatures[] = new PhpFeature( 185 | PhpVersion::PHP_84, 186 | 'Property hooks', 187 | fn (Node $node): bool => $node instanceof PropertyHook, 188 | ); 189 | 190 | // match 191 | $this->phpFeatures[] = new PhpFeature( 192 | PhpVersion::PHP_80, 193 | 'Match expression', 194 | fn (Node $node): bool => $node instanceof Match_, 195 | ); 196 | 197 | $this->phpFeatures[] = new PhpFeature( 198 | PhpVersion::PHP_80, 199 | 'Nullsafe method call/property fetch', 200 | fn (Node $node): bool => $node instanceof NullsafeMethodCall || $node instanceof NullsafePropertyFetch, 201 | ); 202 | 203 | // attributes 204 | $this->phpFeatures[] = new PhpFeature( 205 | PhpVersion::PHP_80, 206 | 'Attributes', 207 | fn (Node $node): bool => $node instanceof AttributeGroup, 208 | ); 209 | 210 | // throw expression 211 | $this->phpFeatures[] = new PhpFeature( 212 | PhpVersion::PHP_80, 213 | 'Throw expression', 214 | fn (Node $node): bool => $node instanceof Throw_, 215 | ); 216 | 217 | // enums 218 | $this->phpFeatures[] = new PhpFeature( 219 | PhpVersion::PHP_81, 220 | 'Enums', 221 | fn (Node $node): bool => $node instanceof Enum_, 222 | ); 223 | 224 | // promoted properties 225 | $this->phpFeatures[] = new PhpFeature( 226 | PhpVersion::PHP_80, 227 | 'Promoted properties', 228 | fn (Node $node): bool => $node instanceof Param && $node->isPromoted(), 229 | ); 230 | 231 | } 232 | 233 | /** 234 | * @return PhpFeature[] 235 | */ 236 | public function getPhpFeatures(): array 237 | { 238 | // sort by php version first, just to normalize order 239 | usort( 240 | $this->phpFeatures, 241 | fn (PhpFeature $firstPhpFeature, PhpFeature $secondPhpFeature): int => version_compare( 242 | $firstPhpFeature->getPhpVersion(), 243 | $secondPhpFeature->getPhpVersion() 244 | ) 245 | ); 246 | 247 | return $this->phpFeatures; 248 | } 249 | 250 | private function isNullableUnionType(UnionType $unionType): bool 251 | { 252 | if (count($unionType->types) !== 2) { 253 | return false; 254 | } 255 | 256 | // has `Null`? 257 | foreach ($unionType->types as $type) { 258 | if ($type instanceof Identifier && strtolower($type->name) === 'null') { 259 | return true; 260 | } 261 | } 262 | 263 | return false; 264 | } 265 | } 266 | --------------------------------------------------------------------------------