Skip to content

[FEATURE] Opt-in documentation lint (PoC for #1157) - #1295

Draft
CybotTM wants to merge 2 commits into
TYPO3-Documentation:mainfrom
CybotTM:feature/1157-lint-poc
Draft

[FEATURE] Opt-in documentation lint (PoC for #1157)#1295
CybotTM wants to merge 2 commits into
TYPO3-Documentation:mainfrom
CybotTM:feature/1157-lint-poc

Conversation

@CybotTM

@CybotTM CybotTM commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Draft / RFC. A concrete, working linter for #1157 to anchor the discussion. It ships several rules end-to-end plus the architecture and safety model; the open questions below are for review.

What #1157 is asking for

#1157 lists documentation-content quality rules. render-guides renders all TYPO3 docs — including third-party extension docs — so these checks apply to extension docs too.

⚠️ The hard problem: no back-channel to third-party authors

  • render-guides renders documentation for third-party extensions whose authors we have no reliable way to reach.
  • If a rule starts failing an extension's render, that author likely never finds out.
  • So linting must never silently reject or break third-party docs.

Baked into every rule: opt-in (nothing runs unless the lint setting is truthy, default off) and warning severity (advisory; only aborts a render with an explicit --fail-on-log).

Open question for maintainers: where should results surface so extension authors actually see them (render report, dashboard, annotation, pipeline notification)? That should precede making any rule fatal by default.

Scope: three kinds of rule, only two belong here

A core outcome of the discussion on #1157 — the candidate rules split into three buckets, and not all belong in the renderer:

Bucket Examples Home
Artifact integrity refs, menu/toctree, anchors/permalinks render-guides (it already warns on most)
Content / editorial style discouraged phrases, sentence-case, heading-level structure docs.typo3.org house style; needs RST/AST awareness → render-guides is a pragmatic host
Source-file hygiene tabs/spaces, EOL, final newline, line length .editorconfig + editorconfig-checker + IDE — NOT the renderer

The repo's own .editorconfig already declares indent_style, trim_trailing_whitespace, insert_final_newline and max_line_length = 80 for *.rst. So source-hygiene rules in the renderer would just reimplement it against bytes that don't affect output. This PR therefore does not lint whitespace or line length. Recommended follow-up: wire editorconfig-checker into CI (it currently isn't).

Rules implemented

AST heading rules (extend AbstractHeadingLintTransformer):

  • discouraged phrases — word-bounded, case-insensitive; list via lint_discouraged_phrases.
  • sentence-case — conservative: ignores first word, ALL-CAPS acronyms (TYPO3, API), CamelCase (ViewHelper) and a proper-noun allow list (lint_heading_allowed_words); fires only on ≥2 Title Case words.
  • missing-anchor — warns when a sub-heading has no .. _label: (document title exempt).

Source-level rule (PreParseDocument listener — for what the AST can't see):

  • skipped heading levels — the parser normalises heading levels (a level-3 underline under the title renders as <h2>), so a skip is invisible in the AST and must be read from the raw adornment sequence. Detection is limited to column 0, so ===/--- inside code/literal blocks are never misread as headings.

A note on a subtlety verified during review: missing-anchor works because the upstream MoveAnchorTransformer (priority 30000) relocates each .. _label: anchor into its section before this rule (priority 1000) runs — confirmed end-to-end (an anchored heading is not flagged).

Settings

<extension class="\T3Docs\Typo3DocsTheme\DependencyInjection\Typo3DocsThemeExtension"
           lint="true"
           lint_discouraged_phrases="Non-Composer mode, blacklist"
           lint_heading_allowed_words="Camino, Extbase" />

Tests

Unit tests for every rule and the source-lint listener (the opt-in gate), incl. false-positive guards (acronyms/CamelCase/allow-list; code-block adornments not misread as headings). Integration tests drive the AST rules and the source rule through the real render pipeline (incl. the negative: an anchored heading is not flagged).

Verified via the project's make (Docker/PHP 8.2): unit 120, integration 118, make code-style clean, PHPStan clean, guides.xml validates.

Next steps (RFC discussion, not in this PR)

  • Decide the results-surfacing / back-channel approach.
  • Per-rule enable/severity configuration (today: one master lint switch).
  • Wire editorconfig-checker into CI to cover the source-hygiene bucket.

Refs #1157

@CybotTM
CybotTM force-pushed the feature/1157-lint-poc branch 2 times, most recently from 2d1828c to bcb8e94 Compare June 17, 2026 13:46
@CybotTM

CybotTM commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Ready for feedback

@CybotTM
CybotTM force-pushed the feature/1157-lint-poc branch from 633a0bf to 501eb13 Compare June 18, 2026 09:29
@garvinhicking

Copy link
Copy Markdown
Contributor

This is really good. Thank you!

I fully agree it must be opt-in, good you thought of that too. For the TYPO3 core documentation manuals we'll likely enforce this in the runTests integration. It might be that we could use a kind of "baseline" for these though, because probably very old changelog files that we carry around are not important enough to be fixed and adjusted, whereas new documentation should apply the rules. So for the Core, having this Opt-in would be helpful because I agree not all extension authors will follow-up on "pickyness" for their own docs.

I think it might be helpful if we encapsulate this inside either a subtree split sub-package inside this monorepo, but probably even better to outsource this, so we can better iterate on rules - and then require it as a package?

On the rules themselves, they sound a very good first approach. Most pressing things are the missing headlines to me, so that's very helpful to have those included already. I think the whole body though should be checked for "forbidden" words, not just headlines. And one would include it inside a baseline entry (or ignore the whole rule) if not needed.

It might also be helpful if we could disable rules inside a ReST file with some directive, what do you think?

Not trying to pile on too much on this PR here, I do think we could do it iteratively. That's why a "custom package" sounds helpful to me?

@CybotTM
CybotTM force-pushed the feature/1157-lint-poc branch 4 times, most recently from 1b7f0b7 to a12fb52 Compare June 24, 2026 15:31
CybotTM added 2 commits July 1, 2026 14:00
…1157)

Adds a proof-of-concept documentation linter that hooks into the compiler
pipeline as a node transformer. As a first rule it warns when a section
heading contains a discouraged phrase (e.g. "Non-Composer mode").

The linter is opt-in (the `lint` theme setting, default off) and emits
warnings only, so it never breaks or rejects third-party extension
documentation unless a caller explicitly enables it and opts into
--fail-on-log. This is deliberate: there is no reliable back-channel to
third-party extension authors to tell them their docs started failing a
newly added rule.

Matching is case-insensitive and word-bounded so a short phrase does not
match inside a larger word. The configurable phrase list is trimmed and
de-duplicated.

Tests:
- Unit tests for the opt-in toggle, phrase parsing (trim/empty/dedup),
  case-insensitivity and word-boundary matching (no false positives).
- Integration tests for the enabled (warning emitted) and disabled
  (no warning, opt-in respected) cases.

Refs TYPO3-Documentation#1157

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Builds on the lint PoC with the LintRule seam the reviewers anticipated,
now that rules have multiplied.

AST heading rules (extend the new AbstractHeadingLintTransformer, which
owns the opt-in gate and warning channel):
- sentence-case: warns on Title Case headings via a conservative heuristic
  (ignores the first word, ALL-CAPS acronyms, CamelCase identifiers and a
  configurable proper-noun allow list; fires only on two or more Title Case
  words).
- missing-anchor: warns when a sub-heading has no explicit `.. _label:`
  anchor (the document title is exempt).

Source-level rule (runs on the raw reStructuredText via a PreParseDocument
listener, for a concern the parsed AST cannot see):
- skipped heading levels: the parser normalises heading levels, so a skip
  is only detectable from the authored adornment sequence in the source.
  Detection is limited to column 0, so adornments inside code/literal
  blocks are never misread as headings.

Pure source-hygiene checks (tabs, trailing whitespace, line length) are
deliberately NOT done here: the repo's own .editorconfig already declares
them for *.rst, so they belong to editorconfig-checker, not the renderer.

The shared truthy-flag parsing lives in Typo3DocsThemeSettings::isEnabled().

All rules remain opt-in (the `lint` setting, default off) and warning-only,
so third-party extension documentation is never broken by default.

Adds unit tests for every rule and the source-lint listener, plus
integration tests exercising the AST and source rules through the real
render pipeline.

Refs TYPO3-Documentation#1157

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@CybotTM
CybotTM force-pushed the feature/1157-lint-poc branch from a12fb52 to 3f48ba9 Compare July 1, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants