diff --git a/packages/cli/src/commands/__tests__/lint.test.ts b/packages/cli/src/commands/__tests__/lint.test.ts index 69d59fb82..2390d1b0d 100644 --- a/packages/cli/src/commands/__tests__/lint.test.ts +++ b/packages/cli/src/commands/__tests__/lint.test.ts @@ -81,6 +81,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); }); @@ -94,6 +95,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -106,6 +108,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -118,6 +121,7 @@ describe('lint', () => { output: { json: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -184,6 +188,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: true, failOnUnmatchedGlobs: false, + showDocumentationUrl: false, }); }); @@ -195,6 +200,7 @@ describe('lint', () => { output: { stylish: '' }, ignoreUnknownFormat: false, failOnUnmatchedGlobs: true, + showDocumentationUrl: false, }); }); @@ -244,13 +250,13 @@ describe('lint', () => { expect(process.stderr.write).nthCalledWith(2, `Error #1: ${chalk.red('some unhandled exception')}\n`); expect(process.stderr.write).nthCalledWith( 3, - expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:236`), + expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:242`), ); expect(process.stderr.write).nthCalledWith(4, `Error #2: ${chalk.red('another one')}\n`); expect(process.stderr.write).nthCalledWith( 5, - expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:237`), + expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:243`), ); expect(process.stderr.write).nthCalledWith(6, `Error #3: ${chalk.red('original exception')}\n`); diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index dffc1f3bb..d84c6f89e 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -151,6 +151,11 @@ const lintCommand: CommandModule = { type: 'boolean', default: false, }, + 'show-documentation-url': { + description: 'show documentation url in output result', + type: 'boolean', + default: false, + }, verbose: { alias: 'v', description: 'increase verbosity', @@ -175,6 +180,7 @@ const lintCommand: CommandModule = { encoding, ignoreUnknownFormat, failOnUnmatchedGlobs, + showDocumentationUrl, ...config } = args as unknown as ILintConfig & { documents: Array; @@ -189,6 +195,7 @@ const lintCommand: CommandModule = { encoding, ignoreUnknownFormat, failOnUnmatchedGlobs, + showDocumentationUrl, ruleset, stdinFilepath, ...pick, keyof ILintConfig>(config, ['verbose', 'quiet', 'resolver']), @@ -198,6 +205,10 @@ const lintCommand: CommandModule = { linterResult.results = filterResultsBySeverity(linterResult.results, failSeverity); } + if (!showDocumentationUrl) { + linterResult.results = removeDocumentationUrlFromResults(linterResult.results); + } + await Promise.all( format.map(f => { const formattedOutput = formatOutput( @@ -279,6 +290,10 @@ const filterResultsBySeverity = (results: IRuleResult[], failSeverity: FailSever return results.filter(r => r.severity <= diagnosticSeverity); }; +const removeDocumentationUrlFromResults = (results: IRuleResult[]): IRuleResult[] => { + return results.map(r => ({ ...r, documentationUrl: undefined })); +}; + export const severeEnoughToFail = (results: IRuleResult[], failSeverity: FailSeverity): boolean => { const diagnosticSeverity = getDiagnosticSeverity(failSeverity); return results.some(r => r.severity <= diagnosticSeverity); diff --git a/packages/cli/src/services/config.ts b/packages/cli/src/services/config.ts index b0ec0213c..d2dcf5849 100644 --- a/packages/cli/src/services/config.ts +++ b/packages/cli/src/services/config.ts @@ -24,6 +24,7 @@ export interface ILintConfig { stdinFilepath?: string; ignoreUnknownFormat: boolean; failOnUnmatchedGlobs: boolean; + showDocumentationUrl: boolean; verbose?: boolean; quiet?: boolean; } diff --git a/packages/core/src/runner/lintNode.ts b/packages/core/src/runner/lintNode.ts index 2dcf931c2..6070c6811 100644 --- a/packages/core/src/runner/lintNode.ts +++ b/packages/core/src/runner/lintNode.ts @@ -99,6 +99,7 @@ function processTargetResults( severity, ...(source !== null ? { source } : null), range, + documentationUrl: rule.documentationUrl ?? undefined, }); } } diff --git a/packages/core/src/types/spectral.ts b/packages/core/src/types/spectral.ts index 6b30d739e..a6ae3a1a2 100644 --- a/packages/core/src/types/spectral.ts +++ b/packages/core/src/types/spectral.ts @@ -13,6 +13,7 @@ export interface IRunOpts { export interface ISpectralDiagnostic extends IDiagnostic { path: JsonPath; code: string | number; + documentationUrl?: string; } export type IRuleResult = ISpectralDiagnostic; diff --git a/packages/formatters/src/__tests__/html.test.ts b/packages/formatters/src/__tests__/html.test.ts index 8db9acb7e..c9d7c4564 100644 --- a/packages/formatters/src/__tests__/html.test.ts +++ b/packages/formatters/src/__tests__/html.test.ts @@ -18,36 +18,42 @@ describe('HTML formatter', () => { 3:10 hint Info object should contain \`contact\` object. + 3:10 warning OpenAPI object info \`description\` must be present and non-empty string. + 5:14 error Info must contain Stoplight + 17:13 information Operation \`description\` must be present and non-empty string. + 64:14 information Operation \`description\` must be present and non-empty string. + 86:13 information Operation \`description\` must be present and non-empty string. + `); }); }); diff --git a/packages/formatters/src/github-actions.ts b/packages/formatters/src/github-actions.ts index f09b43adf..bca7f05c6 100644 --- a/packages/formatters/src/github-actions.ts +++ b/packages/formatters/src/github-actions.ts @@ -41,7 +41,9 @@ export const githubActions: Formatter = results => { // FIXME: Use replaceAll instead after removing Node.js 14 support. const message = result.message.replace(/\n/g, '%0A'); - return `::${OUTPUT_TYPES[result.severity]} ${paramsString}::${message}`; + return `::${OUTPUT_TYPES[result.severity]} ${paramsString}::${message}${ + result.documentationUrl ? `::${result.documentationUrl}` : '' + }`; }) .join('\n'); }; diff --git a/packages/formatters/src/html/html-template-message.html b/packages/formatters/src/html/html-template-message.html index 73f5ba9d1..74ac39f98 100644 --- a/packages/formatters/src/html/html-template-message.html +++ b/packages/formatters/src/html/html-template-message.html @@ -2,4 +2,5 @@ <%= line %>:<%= character %> <%= severity %> <%- message %> + <% if(documentationUrl) { %>documentation<% } %> diff --git a/packages/formatters/src/html/index.ts b/packages/formatters/src/html/index.ts index f68f9d561..33bda5257 100644 --- a/packages/formatters/src/html/index.ts +++ b/packages/formatters/src/html/index.ts @@ -50,6 +50,7 @@ function renderMessages(messages: IRuleResult[], parentIndex: number): string { severity: getSeverityName(message.severity), message: message.message, code: message.code, + documentationUrl: message.documentationUrl, }); }) .join('\n'); diff --git a/packages/formatters/src/json.ts b/packages/formatters/src/json.ts index 4ff9fbce9..0f57fc2f0 100644 --- a/packages/formatters/src/json.ts +++ b/packages/formatters/src/json.ts @@ -2,6 +2,12 @@ import { Formatter } from './types'; export const json: Formatter = results => { const outputJson = results.map(result => { + let documentationUrlObject = {}; + if (result.documentationUrl) { + documentationUrlObject = { + documentationUrl: result.documentationUrl, + }; + } return { code: result.code, path: result.path, @@ -9,6 +15,7 @@ export const json: Formatter = results => { severity: result.severity, range: result.range, source: result.source, + ...documentationUrlObject, }; }); return JSON.stringify(outputJson, null, '\t'); diff --git a/packages/formatters/src/junit.ts b/packages/formatters/src/junit.ts index 646341af5..60eebf2c3 100644 --- a/packages/formatters/src/junit.ts +++ b/packages/formatters/src/junit.ts @@ -62,6 +62,9 @@ export const junit: Formatter = (results, { failSeverity }) => { output += `line ${result.range.start.line + 1}, col ${result.range.start.character + 1}, `; output += `${prepareForCdata(result.message)} (${result.code}) `; output += `at path ${prepareForCdata(path)}`; + if (result.documentationUrl) { + output += `, ${result.documentationUrl}`; + } output += ']]>'; output += ``; output += '\n'; diff --git a/packages/formatters/src/pretty.ts b/packages/formatters/src/pretty.ts index 3d1a40403..a4fd63906 100644 --- a/packages/formatters/src/pretty.ts +++ b/packages/formatters/src/pretty.ts @@ -74,6 +74,11 @@ export const pretty: Formatter = results => { { text: chalk[color].bold(result.code), padding: PAD_TOP0_LEFT2, width: COLUMNS[2] }, { text: chalk.gray(result.message), padding: PAD_TOP0_LEFT2, width: COLUMNS[3] }, { text: chalk.cyan(printPath(result.path, PrintStyle.Dot)), padding: PAD_TOP0_LEFT2 }, + { + text: chalk.gray(result.documentationUrl ?? ''), + padding: PAD_TOP0_LEFT2, + width: result.documentationUrl ? undefined : 0.1, + }, ); ui.div(); }); diff --git a/packages/formatters/src/stylish.ts b/packages/formatters/src/stylish.ts index 7f0aecf34..013eb6439 100644 --- a/packages/formatters/src/stylish.ts +++ b/packages/formatters/src/stylish.ts @@ -72,6 +72,7 @@ export const stylish: Formatter = results => { result.code ?? '', result.message, printPath(result.path, PrintStyle.Dot), + result.documentationUrl ?? '', ]); output += `${table(pathTableData, { diff --git a/packages/formatters/src/teamcity.ts b/packages/formatters/src/teamcity.ts index 8b3a53e32..c69bf5df5 100644 --- a/packages/formatters/src/teamcity.ts +++ b/packages/formatters/src/teamcity.ts @@ -23,7 +23,8 @@ function inspectionType(result: IRuleResult & { source: string }): string { const code = escapeString(result.code); const severity = getSeverityName(result.severity); const message = escapeString(result.message); - return `##teamcity[inspectionType category='openapi' id='${code}' name='${code}' description='${severity} -- ${message}']`; + const documentationUrl = result.documentationUrl ? ` -- ${escapeString(result.documentationUrl)}` : ''; + return `##teamcity[inspectionType category='openapi' id='${code}' name='${code}' description='${severity} -- ${message}${documentationUrl}']`; } function inspection(result: IRuleResult & { source: string }): string { diff --git a/packages/formatters/src/text.ts b/packages/formatters/src/text.ts index 12638bf0c..54705272c 100644 --- a/packages/formatters/src/text.ts +++ b/packages/formatters/src/text.ts @@ -12,7 +12,8 @@ function renderResults(results: IRuleResult[]): string { const line = result.range.start.line + 1; const character = result.range.start.character + 1; const severity = getSeverityName(result.severity); - return `${result.source}:${line}:${character} ${severity} ${result.code} "${result.message}"`; + const documentationUrl = result.documentationUrl ? ` ${result.documentationUrl}` : ''; + return `${result.source}:${line}:${character} ${severity} ${result.code} "${result.message}"${documentationUrl}`; }) .join('\n'); } diff --git a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts index d530b9930..11c9ebc9a 100644 --- a/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts +++ b/packages/rulesets/src/asyncapi/functions/__tests__/asyncApi2DocumentSchema.test.ts @@ -43,6 +43,7 @@ describe('asyncApi2DocumentSchema', () => { ).toEqual([ { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"info" property must have required property "title"', path: ['info'], severity: DiagnosticSeverity.Error, @@ -131,6 +132,7 @@ describe('asyncApi2DocumentSchema', () => { ).toEqual([ { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"0" property type must be string', path: ['channels', '/user/signedup', 'servers', '0'], severity: DiagnosticSeverity.Error, @@ -138,6 +140,7 @@ describe('asyncApi2DocumentSchema', () => { }, { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"2" property type must be string', path: ['channels', '/user/signedup', 'servers', '2'], severity: DiagnosticSeverity.Error, @@ -184,6 +187,7 @@ describe('asyncApi2DocumentSchema', () => { ).toEqual([ { code: 'asyncapi-schema', + documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md#asyncapi-schema', message: '"kafka" property must have required property "url"', path: ['components', 'servers', 'kafka'], severity: DiagnosticSeverity.Error, diff --git a/test-harness/scenarios/formats/results-format-html.scenario b/test-harness/scenarios/formats/results-format-html.scenario index 63ae922d7..75342e396 100644 --- a/test-harness/scenarios/formats/results-format-html.scenario +++ b/test-harness/scenarios/formats/results-format-html.scenario @@ -163,18 +163,21 @@ info: 1:1 warning "servers" must be present and non-empty array. + 2:6 warning Info object must have a "contact" object. + 2:6 warning Info "description" must be present and non-empty string. +