$line) { if (strpos($line, '@') !== false && preg_match('/^ *\*?\s*@\w/', $line)) { $last = $k; } elseif (trim($line) === '') { $last = false; } elseif ($last !== false) { $old_last_line = $lines[$last]; $lines[$last] = $old_last_line . "\n" . $line; unset($lines[$k]); } } $line_offset = 0; foreach ($lines as $k => $line) { $original_line_length = strlen($line); if ($has_r === true) { $line = str_replace("\r", '', $line); } if ($first_line_padding === null) { $asterisk_pos = strpos($line, '*'); if ($asterisk_pos) { $first_line_padding = substr($line, 0, $asterisk_pos - 1); } } if (preg_match('/^ *\*?\s*@([\w\-\\\:]+) *(.*)$/sm', $line, $matches, PREG_OFFSET_CAPTURE)) { /** @var array $matches */ [, $type_info, $data_info] = $matches; [$type] = $type_info; [$data, $data_offset] = $data_info; if (strpos($data, '*')) { $data = rtrim(preg_replace('/^ *\*\s*$/m', '', $data)); } if (empty($special[$type])) { $special[$type] = []; } $data_offset += $line_offset; $special[$type][$data_offset + 3 + $offsetStart] = $data; unset($lines[$k]); } else { // Strip the leading *, if present. // technically only need to preg_replace('/^ *\*/', '', $lines[$k], 1) // however it's slower and removing all spaces and * is fine $lines[$k] = ltrim($lines[$k], ' *'); } $line_offset += $original_line_length + 1; } // Smush the whole docblock to the left edge. $min_indent = 80; $reached_first_non_empty_line = false; foreach ($lines as $k => $line) { $indent = strspn($line, ' '); if ($indent === strlen($line)) { // This line consists of only spaces. Trim it completely. if ($reached_first_non_empty_line === false) { // remove any leading empty lines here, to avoid a preg_replace later unset($lines[$k]); continue; } $lines[$k] = ''; continue; } $reached_first_non_empty_line = true; $min_indent = min($indent, $min_indent); } if ($min_indent > 0) { foreach ($lines as $k => $line) { if (strlen($line) < $min_indent) { continue; } $lines[$k] = substr($line, $min_indent); } } $docblock = implode("\n", $lines); $docblock = rtrim($docblock); $parsed = new ParsedDocblock($docblock, $special, $first_line_padding ?: ''); self::resolveTags($parsed); return $parsed; } private static function resolveTags(ParsedDocblock $docblock): void { if (isset($docblock->tags['template']) || isset($docblock->tags['psalm-template']) || isset($docblock->tags['phpstan-template']) ) { $docblock->combined_tags['template'] = ($docblock->tags['template'] ?? []) + ($docblock->tags['phpstan-template'] ?? []) + ($docblock->tags['psalm-template'] ?? []); } if (isset($docblock->tags['template-covariant']) || isset($docblock->tags['psalm-template-covariant']) || isset($docblock->tags['phpstan-template-covariant']) ) { $docblock->combined_tags['template-covariant'] = ($docblock->tags['template-covariant'] ?? []) + ($docblock->tags['phpstan-template-covariant'] ?? []) + ($docblock->tags['psalm-template-covariant'] ?? []); } if (isset($docblock->tags['template-extends']) || isset($docblock->tags['inherits']) || isset($docblock->tags['extends']) || isset($docblock->tags['psalm-extends']) || isset($docblock->tags['phpstan-extends']) ) { $docblock->combined_tags['extends'] = ($docblock->tags['template-extends'] ?? []) + ($docblock->tags['inherits'] ?? []) + ($docblock->tags['extends'] ?? []) + ($docblock->tags['psalm-extends'] ?? []) + ($docblock->tags['phpstan-extends'] ?? []); } if (isset($docblock->tags['template-implements']) || isset($docblock->tags['implements']) || isset($docblock->tags['phpstan-implements']) || isset($docblock->tags['psalm-implements']) ) { $docblock->combined_tags['implements'] = ($docblock->tags['template-implements'] ?? []) + ($docblock->tags['implements'] ?? []) + ($docblock->tags['phpstan-implements'] ?? []) + ($docblock->tags['psalm-implements'] ?? []); } if (isset($docblock->tags['template-use']) || isset($docblock->tags['use']) || isset($docblock->tags['phpstan-use']) || isset($docblock->tags['psalm-use']) ) { $docblock->combined_tags['use'] = ($docblock->tags['template-use'] ?? []) + ($docblock->tags['use'] ?? []) + ($docblock->tags['phpstan-use'] ?? []) + ($docblock->tags['psalm-use'] ?? []); } if (isset($docblock->tags['method']) || isset($docblock->tags['psalm-method']) ) { $docblock->combined_tags['method'] = ($docblock->tags['method'] ?? []) + ($docblock->tags['psalm-method'] ?? []); } if (isset($docblock->tags['return']) || isset($docblock->tags['psalm-return']) || isset($docblock->tags['phpstan-return']) ) { if (isset($docblock->tags['psalm-return'])) { $docblock->combined_tags['return'] = $docblock->tags['psalm-return']; } elseif (isset($docblock->tags['phpstan-return'])) { $docblock->combined_tags['return'] = $docblock->tags['phpstan-return']; } else { $docblock->combined_tags['return'] = $docblock->tags['return']; } } if (isset($docblock->tags['param']) || isset($docblock->tags['psalm-param']) || isset($docblock->tags['phpstan-param']) ) { $docblock->combined_tags['param'] = ($docblock->tags['param'] ?? []) + ($docblock->tags['phpstan-param'] ?? []) + ($docblock->tags['psalm-param'] ?? []); } if (isset($docblock->tags['var']) || isset($docblock->tags['psalm-var']) || isset($docblock->tags['phpstan-var']) ) { if (!isset($docblock->tags['ignore-var']) && !isset($docblock->tags['psalm-ignore-var']) ) { $docblock->combined_tags['var'] = ($docblock->tags['var'] ?? []) + ($docblock->tags['phpstan-var'] ?? []) + ($docblock->tags['psalm-var'] ?? []); } } if (isset($docblock->tags['param-out']) || isset($docblock->tags['psalm-param-out']) || isset($docblock->tags['phpstan-param-out']) ) { $docblock->combined_tags['param-out'] = ($docblock->tags['param-out'] ?? []) + ($docblock->tags['phpstan-param-out'] ?? []) + ($docblock->tags['psalm-param-out'] ?? []); } } /** * @return list * @throws DocblockParseException when a @psalm-internal tag doesn't include a namespace */ public static function handlePsalmInternal(ParsedDocblock $parsed_docblock): array { if (isset($parsed_docblock->tags['psalm-internal'])) { $psalm_internal = array_map("trim", $parsed_docblock->tags['psalm-internal']); if (count($psalm_internal) !== count(array_filter($psalm_internal))) { throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); } // assert($psalm_internal === array_filter($psalm_internal)); // TODO get this to work assert(self::assertArrayOfNonEmptyString($psalm_internal)); return array_values($psalm_internal); } return []; } /** @psalm-assert-if-true array $arr */ private static function assertArrayOfNonEmptyString(array $arr): bool { foreach ($arr as $val) { if (!is_string($val) || $val === "") { return false; } } return true; } }