Skip to content

Commit 6e15f43

Browse files
committed
feat(core): add documentUrl to JS api and cli formatters
1 parent d2b465c commit 6e15f43

33 files changed

+1044
-6
lines changed

docs/guides/2-cli.md

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Other options include:
4545
-D, --display-only-failures only output results equal to or greater than --fail-severity [boolean] [default: false]
4646
--ignore-unknown-format do not warn about unmatched formats [boolean] [default: false]
4747
--fail-on-unmatched-globs fail on unmatched glob patterns [boolean] [default: false]
48+
--show-documentation-url show documentation url in output result [boolean] [default: false]
4849
-v, --verbose increase verbosity [boolean]
4950
-q, --quiet no logging - output only [boolean]
5051
```

packages/cli/src/commands/__tests__/lint.test.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ describe('lint', () => {
8181
output: { stylish: '<stdout>' },
8282
ignoreUnknownFormat: false,
8383
failOnUnmatchedGlobs: false,
84+
showDocumentationUrl: false,
8485
});
8586
});
8687
});
@@ -94,6 +95,7 @@ describe('lint', () => {
9495
output: { stylish: '<stdout>' },
9596
ignoreUnknownFormat: false,
9697
failOnUnmatchedGlobs: false,
98+
showDocumentationUrl: false,
9799
});
98100
});
99101

@@ -106,6 +108,7 @@ describe('lint', () => {
106108
output: { stylish: '<stdout>' },
107109
ignoreUnknownFormat: false,
108110
failOnUnmatchedGlobs: false,
111+
showDocumentationUrl: false,
109112
});
110113
});
111114

@@ -118,6 +121,7 @@ describe('lint', () => {
118121
output: { json: '<stdout>' },
119122
ignoreUnknownFormat: false,
120123
failOnUnmatchedGlobs: false,
124+
showDocumentationUrl: false,
121125
});
122126
});
123127

@@ -184,6 +188,7 @@ describe('lint', () => {
184188
output: { stylish: '<stdout>' },
185189
ignoreUnknownFormat: true,
186190
failOnUnmatchedGlobs: false,
191+
showDocumentationUrl: false,
187192
});
188193
});
189194

@@ -195,6 +200,7 @@ describe('lint', () => {
195200
output: { stylish: '<stdout>' },
196201
ignoreUnknownFormat: false,
197202
failOnUnmatchedGlobs: true,
203+
showDocumentationUrl: false,
198204
});
199205
});
200206

@@ -244,13 +250,13 @@ describe('lint', () => {
244250
expect(process.stderr.write).nthCalledWith(2, `Error #1: ${chalk.red('some unhandled exception')}\n`);
245251
expect(process.stderr.write).nthCalledWith(
246252
3,
247-
expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:236`),
253+
expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:242`),
248254
);
249255

250256
expect(process.stderr.write).nthCalledWith(4, `Error #2: ${chalk.red('another one')}\n`);
251257
expect(process.stderr.write).nthCalledWith(
252258
5,
253-
expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:237`),
259+
expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:243`),
254260
);
255261

256262
expect(process.stderr.write).nthCalledWith(6, `Error #3: ${chalk.red('original exception')}\n`);

packages/cli/src/commands/lint.ts

+15
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ const lintCommand: CommandModule = {
151151
type: 'boolean',
152152
default: false,
153153
},
154+
'show-documentation-url': {
155+
description: 'show documentation url in output result',
156+
type: 'boolean',
157+
default: false,
158+
},
154159
verbose: {
155160
alias: 'v',
156161
description: 'increase verbosity',
@@ -175,6 +180,7 @@ const lintCommand: CommandModule = {
175180
encoding,
176181
ignoreUnknownFormat,
177182
failOnUnmatchedGlobs,
183+
showDocumentationUrl,
178184
...config
179185
} = args as unknown as ILintConfig & {
180186
documents: Array<number | string>;
@@ -189,6 +195,7 @@ const lintCommand: CommandModule = {
189195
encoding,
190196
ignoreUnknownFormat,
191197
failOnUnmatchedGlobs,
198+
showDocumentationUrl,
192199
ruleset,
193200
stdinFilepath,
194201
...pick<Partial<ILintConfig>, keyof ILintConfig>(config, ['verbose', 'quiet', 'resolver']),
@@ -198,6 +205,10 @@ const lintCommand: CommandModule = {
198205
linterResult.results = filterResultsBySeverity(linterResult.results, failSeverity);
199206
}
200207

208+
if (!showDocumentationUrl) {
209+
linterResult.results = removeDocumentationUrlFromResults(linterResult.results);
210+
}
211+
201212
await Promise.all(
202213
format.map(f => {
203214
const formattedOutput = formatOutput(
@@ -279,6 +290,10 @@ const filterResultsBySeverity = (results: IRuleResult[], failSeverity: FailSever
279290
return results.filter(r => r.severity <= diagnosticSeverity);
280291
};
281292

293+
const removeDocumentationUrlFromResults = (results: IRuleResult[]): IRuleResult[] => {
294+
return results.map(r => ({ ...r, documentationUrl: undefined }));
295+
};
296+
282297
export const severeEnoughToFail = (results: IRuleResult[], failSeverity: FailSeverity): boolean => {
283298
const diagnosticSeverity = getDiagnosticSeverity(failSeverity);
284299
return results.some(r => r.severity <= diagnosticSeverity);

packages/cli/src/services/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface ILintConfig {
2424
stdinFilepath?: string;
2525
ignoreUnknownFormat: boolean;
2626
failOnUnmatchedGlobs: boolean;
27+
showDocumentationUrl: boolean;
2728
verbose?: boolean;
2829
quiet?: boolean;
2930
}

packages/core/src/runner/lintNode.ts

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ function processTargetResults(
9999
severity,
100100
...(source !== null ? { source } : null),
101101
range,
102+
documentationUrl: rule.documentationUrl ?? undefined,
102103
});
103104
}
104105
}

packages/core/src/types/spectral.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface IRunOpts {
1313
export interface ISpectralDiagnostic extends IDiagnostic {
1414
path: JsonPath;
1515
code: string | number;
16+
documentationUrl?: string;
1617
}
1718

1819
export type IRuleResult = ISpectralDiagnostic;

packages/formatters/src/__tests__/html.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,42 @@ describe('HTML formatter', () => {
1818
<td>3:10</td>
1919
<td class="severity clr-hint">hint</td>
2020
<td>Info object should contain \`contact\` object.</td>
21+
<td></td>
2122
</tr>
2223
2324
<tr style="display:none" class="f-0">
2425
<td>3:10</td>
2526
<td class="severity clr-warning">warning</td>
2627
<td>OpenAPI object info \`description\` must be present and non-empty string.</td>
28+
<td></td>
2729
</tr>
2830
2931
<tr style="display:none" class="f-0">
3032
<td>5:14</td>
3133
<td class="severity clr-error">error</td>
3234
<td>Info must contain Stoplight</td>
35+
<td></td>
3336
</tr>
3437
3538
<tr style="display:none" class="f-0">
3639
<td>17:13</td>
3740
<td class="severity clr-information">information</td>
3841
<td>Operation \`description\` must be present and non-empty string.</td>
42+
<td></td>
3943
</tr>
4044
4145
<tr style="display:none" class="f-0">
4246
<td>64:14</td>
4347
<td class="severity clr-information">information</td>
4448
<td>Operation \`description\` must be present and non-empty string.</td>
49+
<td></td>
4550
</tr>
4651
4752
<tr style="display:none" class="f-0">
4853
<td>86:13</td>
4954
<td class="severity clr-information">information</td>
5055
<td>Operation \`description\` must be present and non-empty string.</td>
56+
<td></td>
5157
</tr>`);
5258
});
5359
});

packages/formatters/src/github-actions.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export const githubActions: Formatter = results => {
4141
// FIXME: Use replaceAll instead after removing Node.js 14 support.
4242
const message = result.message.replace(/\n/g, '%0A');
4343

44-
return `::${OUTPUT_TYPES[result.severity]} ${paramsString}::${message}`;
44+
return `::${OUTPUT_TYPES[result.severity]} ${paramsString}::${message}${
45+
result.documentationUrl ? `::${result.documentationUrl}` : ''
46+
}`;
4547
})
4648
.join('\n');
4749
};

packages/formatters/src/html/html-template-message.html

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
<td><%= line %>:<%= character %></td>
33
<td class="severity clr-<%= severity %>"><%= severity %></td>
44
<td><%- message %></td>
5+
<td><% if(documentationUrl) { %><a href="<%- documentationUrl %>" target="_blank">documentation</a><% } %></td>
56
</tr>

packages/formatters/src/html/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function renderMessages(messages: IRuleResult[], parentIndex: number): string {
5050
severity: getSeverityName(message.severity),
5151
message: message.message,
5252
code: message.code,
53+
documentationUrl: message.documentationUrl,
5354
});
5455
})
5556
.join('\n');

packages/formatters/src/json.ts

+7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import { Formatter } from './types';
22

33
export const json: Formatter = results => {
44
const outputJson = results.map(result => {
5+
let documentationUrlObject = {};
6+
if (result.documentationUrl) {
7+
documentationUrlObject = {
8+
documentationUrl: result.documentationUrl,
9+
};
10+
}
511
return {
612
code: result.code,
713
path: result.path,
814
message: result.message,
915
severity: result.severity,
1016
range: result.range,
1117
source: result.source,
18+
...documentationUrlObject,
1219
};
1320
});
1421
return JSON.stringify(outputJson, null, '\t');

packages/formatters/src/junit.ts

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export const junit: Formatter = (results, { failSeverity }) => {
6262
output += `line ${result.range.start.line + 1}, col ${result.range.start.character + 1}, `;
6363
output += `${prepareForCdata(result.message)} (${result.code}) `;
6464
output += `at path ${prepareForCdata(path)}`;
65+
if (result.documentationUrl) {
66+
output += `, ${result.documentationUrl}`;
67+
}
6568
output += ']]>';
6669
output += `</failure>`;
6770
output += '</testcase>\n';

packages/formatters/src/pretty.ts

+5
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export const pretty: Formatter = results => {
7474
{ text: chalk[color].bold(result.code), padding: PAD_TOP0_LEFT2, width: COLUMNS[2] },
7575
{ text: chalk.gray(result.message), padding: PAD_TOP0_LEFT2, width: COLUMNS[3] },
7676
{ text: chalk.cyan(printPath(result.path, PrintStyle.Dot)), padding: PAD_TOP0_LEFT2 },
77+
{
78+
text: chalk.gray(result.documentationUrl ?? ''),
79+
padding: PAD_TOP0_LEFT2,
80+
width: result.documentationUrl ? undefined : 0.1,
81+
},
7782
);
7883
ui.div();
7984
});

packages/formatters/src/sarif.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const sarif: Formatter = (results, _, ctx) => {
4444
const severity: DiagnosticSeverity = result.severity || DiagnosticSeverity.Error;
4545
sarifResultBuilder.initSimple({
4646
level: OUTPUT_TYPES[severity] || 'error',
47-
messageText: result.message,
47+
messageText: result.documentationUrl ? `${result.message} -- ${result.documentationUrl}` : result.message,
4848
ruleId: result.code.toString(),
4949
fileUri: relative(process.cwd(), result.source ?? '').replace(/\\/g, '/'),
5050
startLine: result.range.start.line + 1,

packages/formatters/src/stylish.ts

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const stylish: Formatter = results => {
7272
result.code ?? '',
7373
result.message,
7474
printPath(result.path, PrintStyle.Dot),
75+
result.documentationUrl ?? '',
7576
]);
7677

7778
output += `${table(pathTableData, {

packages/formatters/src/teamcity.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ function inspectionType(result: IRuleResult & { source: string }): string {
2323
const code = escapeString(result.code);
2424
const severity = getSeverityName(result.severity);
2525
const message = escapeString(result.message);
26-
return `##teamcity[inspectionType category='openapi' id='${code}' name='${code}' description='${severity} -- ${message}']`;
26+
const documentationUrl = result.documentationUrl ? ` -- ${escapeString(result.documentationUrl)}` : '';
27+
return `##teamcity[inspectionType category='openapi' id='${code}' name='${code}' description='${severity} -- ${message}${documentationUrl}']`;
2728
}
2829

2930
function inspection(result: IRuleResult & { source: string }): string {

packages/formatters/src/text.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ function renderResults(results: IRuleResult[]): string {
1212
const line = result.range.start.line + 1;
1313
const character = result.range.start.character + 1;
1414
const severity = getSeverityName(result.severity);
15-
return `${result.source}:${line}:${character} ${severity} ${result.code} "${result.message}"`;
15+
const documentationUrl = result.documentationUrl ? ` ${result.documentationUrl}` : '';
16+
return `${result.source}:${line}:${character} ${severity} ${result.code} "${result.message}"${documentationUrl}`;
1617
})
1718
.join('\n');
1819
}

packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe('asyncApi2DocumentSchema', () => {
4343
).toEqual([
4444
{
4545
code: 'asyncapi-schema',
46+
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema',
4647
message: '"info" property must have required property "title"',
4748
path: ['info'],
4849
severity: DiagnosticSeverity.Error,
@@ -131,13 +132,15 @@ describe('asyncApi2DocumentSchema', () => {
131132
).toEqual([
132133
{
133134
code: 'asyncapi-schema',
135+
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema',
134136
message: '"0" property type must be string',
135137
path: ['channels', '/user/signedup', 'servers', '0'],
136138
severity: DiagnosticSeverity.Error,
137139
range: expect.any(Object),
138140
},
139141
{
140142
code: 'asyncapi-schema',
143+
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema',
141144
message: '"2" property type must be string',
142145
path: ['channels', '/user/signedup', 'servers', '2'],
143146
severity: DiagnosticSeverity.Error,
@@ -184,6 +187,7 @@ describe('asyncApi2DocumentSchema', () => {
184187
).toEqual([
185188
{
186189
code: 'asyncapi-schema',
190+
documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema',
187191
message: '"kafka" property must have required property "url"',
188192
path: ['components', 'servers', 'kafka'],
189193
severity: DiagnosticSeverity.Error,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
====test====
2+
Invalid document --output to a file, will show all the contents when the file is read
3+
====document====
4+
---
5+
info:
6+
version: 1.0.0
7+
title: Stoplight
8+
====asset:ruleset.json====
9+
{
10+
"rules": {
11+
"api-servers": {
12+
"documentationUrl": "https://www.example.com/docs/api-servers.md",
13+
"description": "\"servers\" must be present and non-empty array.",
14+
"recommended": true,
15+
"given": "$",
16+
"then": {
17+
"field": "servers",
18+
"function": "schema",
19+
"functionOptions": {
20+
"dialect": "draft7",
21+
"schema": {
22+
"items": {
23+
"type": "object",
24+
},
25+
"minItems": 1,
26+
"type": "array"
27+
}
28+
}
29+
}
30+
},
31+
"info-contact": {
32+
"description": "Info object must have a \"contact\" object.",
33+
"recommended": true,
34+
"type": "style",
35+
"given": "$",
36+
"then": {
37+
"field": "info.contact",
38+
"function": "truthy",
39+
}
40+
},
41+
"info-description": {
42+
"documentationUrl": "https://www.example.com/docs/info-description.md",
43+
"description": "Info \"description\" must be present and non-empty string.",
44+
"recommended": true,
45+
"type": "style",
46+
"given": "$",
47+
"then": {
48+
"field": "info.description",
49+
"function": "truthy"
50+
}
51+
}
52+
}
53+
}
54+
====command-nix====
55+
{bin} lint {document} --ruleset "{asset:ruleset.json}" --format=text --output={asset:output.txt} --show-documentation-url > /dev/null; cat {asset:output.txt}
56+
====command-win====
57+
{bin} lint {document} --ruleset "{asset:ruleset.json}" --format=text --output={asset:output.txt} --show-documentation-url | Out-Null; cat {asset:output.txt}
58+
====asset:output.txt====
59+
====stdout====
60+
{document}:1:1 warning api-servers ""servers" must be present and non-empty array." https://www.example.com/docs/api-servers.md
61+
{document}:2:6 warning info-contact "Info object must have a "contact" object."
62+
{document}:2:6 warning info-description "Info "description" must be present and non-empty string." https://www.example.com/docs/info-description.md
63+

0 commit comments

Comments
 (0)