Skip to content

Commit 39f3ad8

Browse files
committed
feat(assertions): support reference on configurable rules and fix github-actions formatting
Addresses @DmitryAnansky's feedback on #2734: - Add `reference: string` to the `ConfigurableRule` schema so `redocly.yaml` rules can set a top-level `reference` field alongside `message` and `suggest`. - Thread `reference` through `buildSubjectVisitor` so assertion failures populate `problem.reference` for the existing stylish and github-actions formatters (which already handle `problem.reference`). - Add `reference?: string` to `RawAssertion` so the TypeScript surface matches the runtime schema. Also fixes the github-actions formatter's double-newline when both `suggest` and `reference` are present (flagged by Cursor Bugbot): `formatDidYouMean()` already returns a string ending with `\n\n`, so the old separator logic produced four consecutive newlines. Match the codeframe pattern: add a single `\n\n` separator before the first section and rely on each formatter's trailing newlines. Tests: - Extend lint tests with a configurable rule that sets `reference`; assert the resulting problem carries the URL through to the output. - Add a github-actions format test covering both `suggest` + `reference` together, asserting a single blank line between each section. - Update changeset to cover configurable rules (with the line-break phrasing JLekawa suggested).
1 parent 1d9b0db commit 39f3ad8

7 files changed

Lines changed: 69 additions & 4 deletions

File tree

.changeset/report-reference-property.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"@redocly/openapi-core": minor
33
---
44

5-
Added a `reference` property to `context.report()` so custom rules can link to external documentation.
5+
Added a `reference` property to `context.report()` so custom rules can link to external documentation. Configurable rules also accept a top-level `reference` field.
66
When set, the URL is rendered beneath the message in both stylish and `github-actions` output formats.

packages/core/src/__tests__/format.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,35 @@ describe('format', () => {
239239
);
240240
});
241241

242+
it('should render both suggestions and reference with single separator in github-actions format', () => {
243+
const problems: NormalizedProblem[] = [
244+
{
245+
ruleId: 'operation-id',
246+
message: 'Operation ID is required',
247+
severity: 'error' as const,
248+
location: [
249+
{
250+
source: { absoluteRef: 'openapi.yaml' } as Source,
251+
start: { line: 1, col: 2 },
252+
end: { line: 3, col: 4 },
253+
} as LocationObject,
254+
],
255+
suggest: ['addOperation'],
256+
reference: 'https://wiki.example.com/api-guidelines#operation-id',
257+
},
258+
];
259+
260+
formatProblems(problems, {
261+
format: 'github-actions',
262+
version: '1.0.0',
263+
totals: getTotals(problems),
264+
});
265+
266+
expect(output).toEqual(
267+
'::error title=operation-id,file=openapi.yaml,line=1,col=2,endLine=3,endColumn=4::Operation ID is required%0A%0ADid you mean: addOperation ?%0A%0AReference: https://wiki.example.com/api-guidelines#operation-id%0A%0A\n'
268+
);
269+
});
270+
242271
it('should limit suggestions based on REDOCLY_CLI_LINT_MAX_SUGGESTIONS constant', () => {
243272
const problems: NormalizedProblem[] = [
244273
{

packages/core/src/__tests__/lint.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,41 @@ describe('lint', () => {
390390
expect(replaceSourceWithRef(results_with_createConfig)).toEqual(replaceSourceWithRef(results));
391391
});
392392

393+
it('lintFromString should propagate reference from a configurable rule to the resulting problem', async () => {
394+
const source = outdent`
395+
openapi: 3.0.2
396+
info:
397+
title: Example
398+
version: '1.0'
399+
paths:
400+
/user:
401+
get:
402+
responses:
403+
'200':
404+
description: OK
405+
`;
406+
const results = await lintFromString({
407+
absoluteRef: '/test/spec.yaml',
408+
source,
409+
config: await createConfig(outdent`
410+
rules:
411+
rule/operation-summary-required:
412+
subject:
413+
type: Operation
414+
property: summary
415+
assertions:
416+
defined: true
417+
message: Operation summary is required
418+
severity: error
419+
reference: https://docs.example.com/style-guide#operation-summary
420+
`),
421+
});
422+
423+
const problem = results.find((r) => r.ruleId === 'rule/operation-summary-required');
424+
expect(problem).toBeDefined();
425+
expect(problem?.reference).toEqual('https://docs.example.com/style-guide#operation-summary');
426+
});
427+
393428
it('lint should work', async () => {
394429
const results = await lint({
395430
ref: path.join(__dirname, 'fixtures/lint/openapi.yaml'),

packages/core/src/format/format.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,7 @@ function outputForGithubActions(problems: NormalizedProblem[], cwd: string): voi
443443
const suggest = formatDidYouMean(problem);
444444
const reference = problem.reference ? `Reference: ${problem.reference}\n\n` : '';
445445
const message =
446-
problem.message +
447-
(suggest !== '' ? '\n\n' + suggest : '') +
448-
(reference !== '' ? '\n\n' + reference : '');
446+
problem.message + (suggest !== '' || reference !== '' ? '\n\n' : '') + suggest + reference;
449447
const properties = {
450448
title: problem.ruleId,
451449
file: isAbsoluteUrl(location.source.absoluteRef)

packages/core/src/rules/common/assertions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type RawAssertion = AssertionDefinition & {
3030
where?: AssertionDefinition[];
3131
message?: string;
3232
suggest?: string[];
33+
reference?: string;
3334
severity?: RuleSeverity;
3435
};
3536

packages/core/src/rules/common/assertions/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export function buildSubjectVisitor(assertId: string, assertion: Assertion): Vis
238238
location: getProblemsLocation(problemGroup) || ctx.location,
239239
forceSeverity: assertion.severity || 'error',
240240
suggest: assertion.suggest || [],
241+
reference: assertion.reference,
241242
ruleId: assertId,
242243
});
243244
}

packages/core/src/types/redocly-yaml.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ const ConfigurableRule: NodeType = {
594594
where: listOf('Where'),
595595
message: { type: 'string' },
596596
suggest: { type: 'array', items: { type: 'string' } },
597+
reference: { type: 'string' },
597598
severity: { enum: ['error', 'warn', 'off'] },
598599
},
599600
required: ['subject', 'assertions'],

0 commit comments

Comments
 (0)