diff --git a/.github/scripts/check-links.php b/.github/scripts/check-links.php new file mode 100644 index 0000000..0cc40bf --- /dev/null +++ b/.github/scripts/check-links.php @@ -0,0 +1,177 @@ + número da linha => conteúdo + */ +function linhasForaDeBloco(string $arquivo): array +{ + $fora = []; + $cerca = false; + + foreach (explode("\n", (string) file_get_contents($arquivo)) as $i => $linha) { + if (str_starts_with(ltrim($linha), '```')) { + $cerca = !$cerca; + + continue; + } + + if (!$cerca) { + $fora[$i + 1] = $linha; + } + } + + return $fora; +} + +/** + * âncoras que os títulos daquele arquivo geram. + * + * @param string $arquivo + * @return array + */ +function ancorasDe(string $arquivo): array +{ + $ancoras = []; + + foreach (linhasForaDeBloco($arquivo) as $linha) { + if (preg_match('/^#{1,6} (.+)$/u', $linha, $m) === 1) { + $ancoras[] = ancoraDe($m[1]); + } + } + + return $ancoras; +} + +/* todo markdown do repositório, menos o que não é nosso */ +$arquivos = []; +$arvore = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + new RecursiveDirectoryIterator($raiz, FilesystemIterator::SKIP_DOTS), + static fn (SplFileInfo $item): bool => !in_array($item->getFilename(), $pular, true) + ) +); + +foreach ($arvore as $item) { + if ($item instanceof SplFileInfo && $item->getExtension() === 'md') { + $arquivos[] = $item->getPathname(); + } +} + +sort($arquivos); + +$cache = []; +$conferidos = 0; +$quebrados = []; + +foreach ($arquivos as $arquivo) { + foreach (linhasForaDeBloco($arquivo) as $numero => $linha) { + if (preg_match_all('/\[[^\]]*\]\(([^)\s]+)\)/u', $linha, $achados) === 0) { + continue; + } + + foreach ($achados[1] as $destino) { + if (preg_match('#^(https?|mailto|tel):#i', $destino) === 1) { + continue; + } + + $conferidos++; + [$caminho, $ancora] = array_pad(explode('#', $destino, 2), 2, ''); + $relativo = str_replace($raiz . '/', '', $arquivo); + + if ($caminho === '') { + $alvo = $arquivo; + } else { + $alvo = realpath(dirname($arquivo) . '/' . $caminho); + + if ($alvo === false) { + $quebrados[] = sprintf('%s:%d %s — não existe', $relativo, $numero, $destino); + + continue; + } + + if (!str_starts_with($alvo, $raiz)) { + $quebrados[] = sprintf('%s:%d %s — sai do repositório', $relativo, $numero, $destino); + + continue; + } + } + + if ($ancora === '' || !str_ends_with($alvo, '.md')) { + continue; + } + + $cache[$alvo] ??= ancorasDe($alvo); + + if (!in_array($ancora, $cache[$alvo], true)) { + $quebrados[] = sprintf( + '%s:%d %s — nenhum título gera essa âncora em %s', + $relativo, + $numero, + $destino, + str_replace($raiz . '/', '', $alvo) + ); + } + } + } +} + +printf("%d arquivos, %d links relativos conferidos\n", count($arquivos), $conferidos); + +if ($quebrados !== []) { + printf("\n%d quebrado(s):\n", count($quebrados)); + + foreach ($quebrados as $quebrado) { + printf(" %s\n", $quebrado); + } + + exit(1); +} + +echo "nenhum link quebrado\n"; diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98e3c18..da590ce 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,6 +43,12 @@ jobs: - name: Static analysis (PHPStan) run: composer test:types + # a documentação são 17 arquivos com link relativo entre eles; link + # quebrado não aparece em teste nem em análise estática, só no 404 de + # quem clicar. + - name: Links da documentação + run: composer test:links + test: name: PHP ${{ matrix.php }} - ${{ matrix.dependencies }} runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 623d364..7334c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,32 @@ O de-para completo da v1 para a v2, quebra por quebra, está no ([#111], [#112]). - Cobertura de testes medida no CI, num job só, com o número no log e no resumo do job. Sem mínimo que reprove o build ([#111], [#112]). +- Verificador de links da documentação no gate, em `composer test:links`. Ele + confere que todo link relativo aponta para arquivo que existe e que toda + âncora corresponde a um título de verdade — título dentro de bloco de código + não conta, que foi como o erro abaixo passou tanto tempo escondido + ([#124]). + +### Alterado + +- **A documentação saiu do README para `docs/`**, uma página por conceito e uma + por gateway, com índice. O README fica com o essencial: o que é, a matriz de + capacidades, requisitos, instalação e início rápido — de 46 KB para 11 KB. + **Link externo que apontava para âncora do README deixa de funcionar**; o + índice de [`docs/`](docs/) cobre todos os destinos ([#113], [#114]). +- A matriz de capacidades ganhou a coluna de **assinatura de webhook**, que + entrou na v2.3.0 e tinha ficado fora da tabela — quem lia era informado de + que a biblioteca fazia menos do que faz ([#113], [#114]). + +### Corrigido + +- **O README renderizava errado da Rede até a Efí.** O bloco de código do + exemplo de recorrência da Cielo nunca era fechado, e cercas alternam: o + título `### Rede` e a prosa embaixo dele saíam como código, os exemplos + seguintes saíam como prosa. Por causa disso, **dez links do sumário** + apontavam para âncoras que não existiam ([#113], [#114]). +- O link para o README no `.github/SUPPORT.md` apontava para `.github/README.md` + ([#113], [#114]). ## [2.3.0] - 2026-10-22 @@ -438,3 +464,6 @@ Primeira versão publicada no Packagist, sob licença MIT. [#102]: https://github.com/phpay-io/phpay/pull/102 [#111]: https://github.com/phpay-io/phpay/issues/111 [#112]: https://github.com/phpay-io/phpay/pull/112 +[#113]: https://github.com/phpay-io/phpay/issues/113 +[#114]: https://github.com/phpay-io/phpay/pull/114 +[#124]: https://github.com/phpay-io/phpay/issues/124 diff --git a/CLAUDE.md b/CLAUDE.md index 6f9eb3a..e840a64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,10 +89,11 @@ Rodar direto no host (requer PHP 8.2+ e Composer): ```bash composer install -composer test # lint + unit + types — mesmo gate do CI +composer test # lint + unit + types + links — mesmo gate do CI composer test:lint # pint --test (PSR-12 + regras do pint.json) composer test:unit # pest composer test:types # phpstan (nível e phpVersion vêm do phpstan.neon) +composer test:links # links relativos e âncoras dos markdowns (.github/scripts/check-links.php) composer lint # pint -v (corrige) ``` diff --git a/composer.json b/composer.json index 6310bb9..bda77a9 100644 --- a/composer.json +++ b/composer.json @@ -73,10 +73,12 @@ "test:lint": "pint --test -v", "test:unit": "pest --colors=always", "test:types": "phpstan analyse", + "test:links": "@php .github/scripts/check-links.php", "test": [ "@test:lint", "@test:unit", - "@test:types" + "@test:types", + "@test:links" ] } }