* Dariusz RumiƄski * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Documentation; use PhpCsFixer\Console\Command\HelpCommand; use PhpCsFixer\Differ\FullDiffer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; use PhpCsFixer\Fixer\DeprecatedFixerInterface; use PhpCsFixer\Fixer\ExperimentalFixerInterface; use PhpCsFixer\Fixer\FixerInterface; use PhpCsFixer\FixerConfiguration\AliasedFixerOption; use PhpCsFixer\FixerConfiguration\AllowedValueSubset; use PhpCsFixer\FixerConfiguration\DeprecatedFixerOptionInterface; use PhpCsFixer\FixerDefinition\CodeSampleInterface; use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface; use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface; use PhpCsFixer\Preg; use PhpCsFixer\RuleSet\RuleSet; use PhpCsFixer\RuleSet\RuleSets; use PhpCsFixer\StdinFileInfo; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Utils; /** * @internal */ final class FixerDocumentGenerator { private DocumentationLocator $locator; private FullDiffer $differ; public function __construct(DocumentationLocator $locator) { $this->locator = $locator; $this->differ = new FullDiffer(); } public function generateFixerDocumentation(FixerInterface $fixer): string { $name = $fixer->getName(); $title = "Rule ``{$name}``"; $titleLine = str_repeat('=', \strlen($title)); $doc = "{$titleLine}\n{$title}\n{$titleLine}"; $definition = $fixer->getDefinition(); $doc .= "\n\n".RstUtils::toRst($definition->getSummary()); $description = $definition->getDescription(); if (null !== $description) { $description = RstUtils::toRst($description); $doc .= <<getSuccessorsNames(); if (0 !== \count($alternatives)) { $deprecationDescription .= RstUtils::toRst(sprintf( "\n\nYou should use %s instead.", Utils::naturalLanguageJoinWithBackticks($alternatives) ), 0); } } $experimentalDescription = ''; if ($fixer instanceof ExperimentalFixerInterface) { $experimentalDescriptionRaw = RstUtils::toRst('Rule is not covered with backward compatibility promise, use it at your own risk. Rule\'s behaviour may be changed at any point, including rule\'s name; its options\' names, availability and allowed values; its default configuration. Rule may be even removed without prior notice. Feel free to provide feedback and help with determining final state of the rule.', 0); $experimentalDescription = <<getRiskyDescription(); if (null !== $riskyDescriptionRaw) { $riskyDescriptionRaw = RstUtils::toRst($riskyDescriptionRaw, 0); $riskyDescription = <<getConfigurationDefinition(); foreach ($configurationDefinition->getOptions() as $option) { $optionInfo = "``{$option->getName()}``"; $optionInfo .= "\n".str_repeat('~', \strlen($optionInfo)); if ($option instanceof DeprecatedFixerOptionInterface) { $deprecationMessage = RstUtils::toRst($option->getDeprecationMessage()); $optionInfo .= "\n\n.. warning:: This option is deprecated and will be removed in the next major version. {$deprecationMessage}"; } $optionInfo .= "\n\n".RstUtils::toRst($option->getDescription()); if ($option instanceof AliasedFixerOption) { $optionInfo .= "\n\n.. note:: The previous name of this option was ``{$option->getAlias()}`` but it is now deprecated and will be removed in the next major version."; } $allowed = HelpCommand::getDisplayableAllowedValues($option); if (null === $allowed) { $allowedKind = 'Allowed types'; $allowed = array_map( static fn ($value): string => '``'.$value.'``', $option->getAllowedTypes(), ); } else { $allowedKind = 'Allowed values'; $allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset ? 'a subset of ``'.Utils::toString($value->getAllowedValues()).'``' : '``'.Utils::toString($value).'``', $allowed); } $allowed = Utils::naturalLanguageJoin($allowed, ''); $optionInfo .= "\n\n{$allowedKind}: {$allowed}"; if ($option->hasDefault()) { $default = Utils::toString($option->getDefault()); $optionInfo .= "\n\nDefault value: ``{$default}``"; } else { $optionInfo .= "\n\nThis option is required."; } $doc .= "\n\n{$optionInfo}"; } } $samples = $definition->getCodeSamples(); if (0 !== \count($samples)) { $doc .= <<<'RST' Examples -------- RST; foreach ($samples as $index => $sample) { $title = sprintf('Example #%d', $index + 1); $titleLine = str_repeat('~', \strlen($title)); $doc .= "\n\n{$title}\n{$titleLine}"; if ($fixer instanceof ConfigurableFixerInterface) { if (null === $sample->getConfiguration()) { $doc .= "\n\n*Default* configuration."; } else { $doc .= sprintf( "\n\nWith configuration: ``%s``.", Utils::toString($sample->getConfiguration()) ); } } $doc .= "\n".$this->generateSampleDiff($fixer, $sample, $index + 1, $name); } } $ruleSetConfigs = self::getSetsOfRule($name); if ([] !== $ruleSetConfigs) { $plural = 1 !== \count($ruleSetConfigs) ? 's' : ''; $doc .= << $config) { $ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set); $ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/')); $configInfo = (null !== $config) ? " with config:\n\n ``".Utils::toString($config)."``\n" : ''; $doc .= <<`_{$configInfo}\n RST; } } $reflectionObject = new \ReflectionObject($fixer); $className = str_replace('\\', '\\\\', $reflectionObject->getName()); $fileName = $reflectionObject->getFileName(); $fileName = str_replace('\\', '/', $fileName); $fileName = substr($fileName, strrpos($fileName, '/src/Fixer/') + 1); $fileName = "`{$className} <./../../../{$fileName}>`_"; $testFileName = Preg::replace('~.*\K/src/(?=Fixer/)~', '/tests/', $fileName); $testFileName = Preg::replace('~PhpCsFixer\\\\\\\\\K(?=Fixer\\\\\\\\)~', 'Tests\\\\\\\\', $testFileName); $testFileName = Preg::replace('~(?= <|\.php>)~', 'Test', $testFileName); $doc .= <<', $doc); return "{$doc}\n"; } /** * @internal * * @return array> */ public static function getSetsOfRule(string $ruleName): array { $ruleSetConfigs = []; foreach (RuleSets::getSetDefinitionNames() as $set) { $ruleSet = new RuleSet([$set => true]); if ($ruleSet->hasRule($ruleName)) { $ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($ruleName); } } return $ruleSetConfigs; } /** * @param FixerInterface[] $fixers */ public function generateFixersDocumentationIndex(array $fixers): string { $overrideGroups = [ 'PhpUnit' => 'PHPUnit', 'PhpTag' => 'PHP Tag', 'Phpdoc' => 'PHPDoc', ]; usort($fixers, static fn (FixerInterface $a, FixerInterface $b): int => \get_class($a) <=> \get_class($b)); $documentation = <<<'RST' ======================= List of Available Rules ======================= RST; $currentGroup = null; foreach ($fixers as $fixer) { $namespace = Preg::replace('/^.*\\\\(.+)\\\\.+Fixer$/', '$1', \get_class($fixer)); $group = $overrideGroups[$namespace] ?? Preg::replace('/(?<=[[:lower:]])(?=[[:upper:]])/', ' ', $namespace); if ($group !== $currentGroup) { $underline = str_repeat('-', \strlen($group)); $documentation .= "\n\n{$group}\n{$underline}\n"; $currentGroup = $group; } $path = './'.$this->locator->getFixerDocumentationFileRelativePath($fixer); $attributes = []; if ($fixer instanceof DeprecatedFixerInterface) { $attributes[] = 'deprecated'; } if ($fixer instanceof ExperimentalFixerInterface) { $attributes[] = 'experimental'; } if ($fixer->isRisky()) { $attributes[] = 'risky'; } $attributes = 0 === \count($attributes) ? '' : ' *('.implode(', ', $attributes).')*'; $summary = str_replace('`', '``', $fixer->getDefinition()->getSummary()); $documentation .= <<getName()} <{$path}>`_{$attributes} {$summary} RST; } return "{$documentation}\n"; } private function generateSampleDiff(FixerInterface $fixer, CodeSampleInterface $sample, int $sampleNumber, string $ruleName): string { if ($sample instanceof VersionSpecificCodeSampleInterface && !$sample->isSuitableFor(\PHP_VERSION_ID)) { $existingFile = @file_get_contents($this->locator->getFixerDocumentationFilePath($fixer)); if (false !== $existingFile) { Preg::match("/\\RExample #{$sampleNumber}\\R.+?(?\\R\\.\\. code-block:: diff\\R\\R.*?)\\R(?:\\R\\S|$)/s", $existingFile, $matches); if (isset($matches['diff'])) { return $matches['diff']; } } $error = <<getCode(); $tokens = Tokens::fromCode($old); $file = $sample instanceof FileSpecificCodeSampleInterface ? $sample->getSplFileInfo() : new StdinFileInfo(); if ($fixer instanceof ConfigurableFixerInterface) { $fixer->configure($sample->getConfiguration() ?? []); } $fixer->fix($file, $tokens); $diff = $this->differ->diff($old, $tokens->generateCode()); $diff = Preg::replace('/@@[ \+\-\d,]+@@\n/', '', $diff); $diff = Preg::replace('/\r/', '^M', $diff); $diff = Preg::replace('/^ $/m', '', $diff); $diff = Preg::replace('/\n$/', '', $diff); $diff = RstUtils::indent($diff, 3); return <<