feat(cli): lint API descriptions with redocly recheck - #3093
adamaltman wants to merge 16 commits into
Conversation
🦋 Changeset detectedLatest commit: 5821708 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8ad2891 to
5d54e7b
Compare
|
|
||
| The `recheck` command lints Markdown files for prose and structure problems. | ||
| It checks headings, sentences, links, images, and Markdoc tags against a set of rules. | ||
| It also lints the `description` fields of API descriptions. |
There was a problem hiding this comment.
| It also lints the `description` fields of API descriptions. | |
| `recheck` also lints the `description` fields of API descriptions. |
| `redocly recheck` now lints the `description` fields of API descriptions. | ||
| Findings report the source line and column, `.redocly.lint-ignore.yaml` suppresses them by file, rule, and pointer, and the `apiDescriptions.rules` block adjusts rules for descriptions only. |
There was a problem hiding this comment.
| `redocly recheck` now lints the `description` fields of API descriptions. | |
| Findings report the source line and column, `.redocly.lint-ignore.yaml` suppresses them by file, rule, and pointer, and the `apiDescriptions.rules` block adjusts rules for descriptions only. | |
| Improved `redocly recheck` to lint the `description` fields of API descriptions. | |
| You can suppress the findings by file, rule, and pointer, or adjust rules for descriptions only. |
8e829f0 to
49f6fb4
Compare
0d4da07 to
f04433a
Compare
f00701f to
804065c
Compare
32add50 to
0cdd84c
Compare
Descriptions extracted from API documents lint in embedded mode with the apiDescriptions overrides, remap to their source position, carry their pointer, and join the same report and baseline as page findings.
An off rule for descriptions plus a matching --rule name threw and stopped the whole run. Strip off names first, and reject an unknown severity string in an apiDescriptions override.
Every string description, external $ref files included, comes back with its pointer and owning source; summary stays out.
Literal blocks map exactly; folded, multi-line, and escaped scalars anchor to the value start.
The check now stops at the closing quote, so a trailing comment cannot turn an exact mapping into an anchor. A test covers the explicit indentation indicator on a literal block.
API paths and the apis block feed descriptions into the same run as Markdown pages. The ignore file suppresses by file, rule, and pointer.
… API descriptions The baseline takes the same ignore predicate as the lint run, so a suppressed description finding never lands in the baseline and no run reports it stale. The ignore file accepts the short rule name that the report prints. Descriptions that two APIs share through a $ref now report once, and two aliases with the same root walk once. --changed-only filters API descriptions with the same changed set as Markdown pages, and an empty result emits the empty report. An API with no descriptions writes the empty report as well, so --format json always has JSON on stdout. The baseline gate counts embedded files as scanned and adds the description rules that ran to the executed rules. The table report keeps description findings out of the fixable count, and a folded scalar anchors to its first content line.
Classify a requested path into api, unreadable-api, or not-api instead of collapsing a parse failure into a Markdown page. Route unreadable API paths into collectEmbeddedInputs, log the failure as an error, and force exit code 1 when the lint or baseline action otherwise reports zero. Reword a comment that named the retired --exclude-rule flag. Print "nothing to check" for an empty target list instead of a bare colon, and skip the redundant "No markdown files found" line for it. Note in the docs that a description lints only local APIs and that a parse failure fails the run.
…rker consistent Filter description rules by severity and tags only, then apply name selection with matchesRuleName, so a mixed run no longer aborts when apiDescriptions.rules lowers a rule below the requested severity. Skip descriptions reached through a remote $ref when collecting embedded inputs, since their absoluteRef is a URL that baselineKeyMapper and the changed-file filter cannot turn into a stable path, and log how many were skipped. Stop marking a table finding [fixable] when it carries a pointer, since --fix never rewrites inside an API description.
With --generate-baseline, a run that could not read an API description still wrote the baseline before it failed, so a partial baseline could replace a good one. The command now reports the count, writes nothing, and fails.
…nned Reject a rule-object apiDescriptions override with an unknown severity, matching the check already applied to string overrides. Count a parsed API file with no local descriptions as scanned, so a leftover baseline entry for it goes stale instead of staying silent.
The handler scenarios now run the real CLI against fixture directories, so the assertions read the same output a user sees. The command test file keeps only the pure-function tests and mirrors index.ts by name.
Bound the block scalar content search to lines inside the value's end location. An empty or whitespace-only description no longer picks up a sibling key as its content line. When no content line exists, the mapper anchors to the indicator line and column instead.
804065c to
5821708
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5821708. Configure here.
| ); | ||
| // An API description that failed to parse fails the gate even when the | ||
| // lint or baseline action otherwise found nothing to report. | ||
| return failureCount > 0 && exitCode === 0 ? 1 : exitCode; |
There was a problem hiding this comment.
Unreadable APIs trigger false stale baselines
Medium Severity
--generate-baseline stops when an API file does not parse, but a normal lint run still applies the baseline gate. A default recheck walk uses . as scanRoots, and that root matches every baseline file. An unreadable API is not added to apiFiles, so leftover counts for it are reported as stale errors. Those extra errors fail the gate and tell people to regenerate a baseline that this run also refuses to write.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5821708. Configure here.
| if (absoluteRef !== undefined && !isAbsoluteUrl(absoluteRef)) files.add(absoluteRef); | ||
| } | ||
|
|
||
| return { descriptions: collected, files: [...files] }; |
There was a problem hiding this comment.
Broken local refs skip description linting
Medium Severity
resolveDocument records missing or unreadable local $ref targets as errors and does not throw. collectDescriptions never inspects those errors, so those files are omitted from files and their descriptions are never linted. Remote $refs are counted and announced, and a root that does not parse fails the run. A broken local $ref does neither, so the command can exit 0 with incomplete coverage.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5821708. Configure here.
| const overrides = applyDescriptionOverrides(validation.rules, apiDescriptions?.rules); | ||
| if (overrides.errors.length > 0) { | ||
| return { success: false, errors: overrides.errors }; | ||
| } |
There was a problem hiding this comment.
Invalid description overrides are silently ignored
Low Severity
apiDescriptions is stripped before engine schema validation, then accepted if it is any plain object. A typo such as rule instead of rules, a non-object apiDescriptions value, or extra keys produces no config error. Unknown rule names inside rules are rejected, so the same block is strict in one place and silent in the others. Overrides then never apply and descriptions keep the page rule set.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5821708. Configure here.


What/Why/How?
This PR makes
redocly rechecklint thedescriptionfields of API descriptions.Each finding reports the file, line, and column of the description in the source.
Pages and descriptions share one report and one exit code.
Discovery.
With no paths, the command lints the Markdown files under the project root and every API in
apis.With paths, a Markdown file or directory lints as pages, and a file that
detectSpecrecognizes lints its descriptions.An API description that does not parse is an error and fails the run, as in
lint.Extraction.
The CLI walks each API document with the public
openapi-corewalker.A visitor collects every string
descriptionwith its pointer and owning source, external$reffiles included.summarystays out.Engine.
The engine gained one input kind, an embedded input: file, pointer, content, and a position mapper.
runLintlints embedded inputs in embedded mode, so document-shape rules do not run on them.The
apiDescriptions.rulesblock adjusts rules for descriptions only; a name that is not in effect is a config error.Each description finding carries its pointer and reports at the mapped source position.
--fixskips descriptions and says once how many fixable findings it skipped.The count-based baseline covers description findings by file and rule.
Positions (Decision D).
A literal block scalar maps exactly: indicator line plus content line, block indentation plus column.
A folded block anchors to its first content line.
Single-line plain and quoted scalars without escapes map the column exactly; multi-line or escaped scalars anchor to the value start.
JSON follows the quoted rule.
Suppression (Decision E).
Inline
<!-- recheck-disable -->directives work inside a description..redocly.lint-ignore.yamlsuppresses a description finding by file, rule, and pointer; the command reports how many findings it suppressed.The rule key can be the full name or the short name.
The baseline generator applies the same suppression, so the two never disagree.
Selection.
Two APIs that share a
$reffile report a description once.Descriptions in remote
$reffiles are skipped, and the command says how many.An explicit API path with no descriptions still emits the report, so
--format jsonalways prints a document.--readabilitycovers Markdown only and says so once when it skips API descriptions.Known limitation.
Take a rule that is off for descriptions but on for pages.
In a run that covers pages and descriptions together, it can mark a stale baseline entry for an API file.
A description-only run does not.
--generate-baselineclears it, and the message says so.This PR stacks on #3082, which adds the command.
Its base changes to
mainafter #3082 merges.Reference
Decision D sets the position mapping; Decision E sets suppression through directives, the ignore file, and the count baseline.
Testing
Unit tests cover the engine's embedded inputs: remapping, pointer, overrides, ignore predicate, fix skip, and baseline.
They also cover description extraction with
$reffiles and dedup, and every position-mapping rule with a fixture per scalar style.End-to-end fixtures under
tests/e2e/recheck/cover the API-description cases.They exercise an explicit API path,
apisdiscovery, an unreadable API, a YAML page, and a shared$reffile.They also cover an API with no descriptions, the ignore file by full and short name, the readability notice, and the baseline guard.
An OpenAPI file with a finding in a literal block, a
$ref'd description that the ignore file suppresses, and a JSON description with--format json.The Rebilly smoke runs
redocly recheckover the Rebilly description with the defaultrecheck/markdownpreset: 6,463 descriptions and 608 findings in about 21 seconds.Gates:
npm run compile,npm run typecheck,npm run lint,npm run format:check,npm run unit, the recheck e2e suite, andnpm run smoke:rebilly.In
npm run unit, the three pre-existing oas3 timeouts are the only failures, the same as onmain.Screenshots (optional)
Check yourself
Security
Note
Medium Risk
Adds a large CLI/recheck integration path (API walking, position mapping, baselines) and a breaking
ResolvedRecheckConfigfield; behavior is well-tested but touches default recheck runs whenapisis configured.Overview
redocly rechecknow lintsdescriptionfields in OpenAPI/API specs alongside Markdown, with findings reported at the real file line/column (including JSON pointers on each issue).The CLI walks API documents (local
$reffiles included), turns each description into an embedded input for@redocly/recheck, and maps lint positions back into YAML/JSON (literal/folded blocks, quoted scalars, etc.). Default runs with no paths also pick up local APIs fromapis; unreadable specs fail the run; remote$refdescriptions are skipped with a count.The recheck engine adds
lintEmbeddedInputs, merges description findings into the same report/baseline/change-only flow, and resolvesrecheck.apiDescriptions.rulesinto a separatedescriptionRulesset (page rules unchanged).--fixdoes not rewrite API files; suppress via.redocly.lint-ignore.yamlby file, rule, and pointer.Breaking for library users:
ResolvedRecheckConfignow requiresdescriptionRules(replacing rawapiDescriptionRuleson the resolved config).Reviewed by Cursor Bugbot for commit 5821708. Bugbot is set up for automated code reviews on this repo. Configure here.