Skip to content

Commit

Permalink
feat(cli): add sarif formatter (#2532)
Browse files Browse the repository at this point in the history
Co-authored-by: Jakub Rożek <[email protected]>
  • Loading branch information
PhilippHeuer and P0lip authored Sep 15, 2023
1 parent da497a7 commit 959a86a
Show file tree
Hide file tree
Showing 11 changed files with 59 additions and 24 deletions.
4 changes: 2 additions & 2 deletions docs/guides/2-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ Other options include:
[string] [choices: "utf8", "ascii", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1"] [default: "utf8"]
-f, --format formatters to use for outputting results, more than one can be given joining them with
a comma
[string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions"] [default:
"stylish"]
[string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif"]
[default: "stylish"]
-o, --output where to output results, can be a single file name, multiple "output.<format>" or
missing to print to stdout [string]
--stdin-filepath path to a file to pretend that stdin comes from [string]
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"scripts": {
"clean": "rimraf .cache packages/*/{dist,.cache}",
"prebuild": "yarn workspaces foreach run prebuild",
"build": "yarn prebuild && tsc --build ./tsconfig.build.json",
"build": "yarn prebuild && tsc --build ./tsconfig.build.json && yarn postbuild",
"postbuild": "yarn workspaces foreach run postbuild",
"prelint": "yarn workspaces foreach run prelint",
"lint": "yarn prelint && yarn lint.prettier && yarn lint.eslint",
"lint.fix": "yarn lint.prettier --write && yarn lint.eslint --fix",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/__tests__/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('lint', () => {
];

beforeEach(() => {
(lint as jest.Mock).mockResolvedValueOnce(results);
(lint as jest.Mock).mockResolvedValueOnce({ results: results, resolvedRuleset: {} });
(formatOutput as jest.Mock).mockReturnValueOnce('<formatted output>');
(writeOutput as jest.Mock).mockResolvedValueOnce(undefined);
});
Expand Down Expand Up @@ -148,7 +148,7 @@ describe('lint', () => {

it.each(['json', 'stylish'])('calls formatOutput with %s format', async format => {
await run(`lint -f ${format} ./__fixtures__/empty-oas2-document.json`);
expect(formatOutput).toBeCalledWith(results, format, { failSeverity: DiagnosticSeverity.Error });
expect(formatOutput).toBeCalledWith(results, format, { failSeverity: DiagnosticSeverity.Error }, expect.anything());
});

it('writes formatted output to a file', async () => {
Expand Down
15 changes: 10 additions & 5 deletions packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ const lintCommand: CommandModule = {
};

try {
let results = await lint(documents, {
const linterResult = await lint(documents, {
format,
output,
encoding,
Expand All @@ -194,18 +194,23 @@ const lintCommand: CommandModule = {
});

if (displayOnlyFailures) {
results = filterResultsBySeverity(results, failSeverity);
linterResult.results = filterResultsBySeverity(linterResult.results, failSeverity);
}

await Promise.all(
format.map(f => {
const formattedOutput = formatOutput(results, f, { failSeverity: getDiagnosticSeverity(failSeverity) });
const formattedOutput = formatOutput(
linterResult.results,
f,
{ failSeverity: getDiagnosticSeverity(failSeverity) },
linterResult.resolvedRuleset,
);
return writeOutput(formattedOutput, output?.[f] ?? '<stdout>');
}),
);

if (results.length > 0) {
process.exit(severeEnoughToFail(results, failSeverity) ? 1 : 0);
if (linterResult.results.length > 0) {
process.exit(severeEnoughToFail(linterResult.results, failSeverity) ? 1 : 0);
} else if (config.quiet !== true) {
const isErrorSeverity = getDiagnosticSeverity(failSeverity) === DiagnosticSeverity.Error;
process.stdout.write(
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/services/__tests__/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import AggregateError = require('es-aggregate-error');
import * as process from 'process';

import lintCommand from '../../commands/lint';
import { lint } from '../linter';
import { LinterResult, lint } from '../linter';

jest.mock('process');
jest.mock('../output');
Expand All @@ -20,7 +20,7 @@ const invalidRulesetPath = resolve(__dirname, '__fixtures__/ruleset-invalid.js')
const validRulesetPath = resolve(__dirname, '__fixtures__/ruleset-valid.js');
const validOas3SpecPath = resolve(__dirname, './__fixtures__/openapi-3.0-valid.yaml');

async function run(command: string) {
async function run(command: string): Promise<LinterResult['results']> {
const parser = yargs.command(lintCommand);
const { documents, ...opts } = await new Promise<any>((resolve, reject) => {
parser.parse(`${command} --ignore-unknown-format`, {}, (err, argv) => {
Expand All @@ -32,7 +32,7 @@ async function run(command: string) {
});
});

return lint(documents, opts);
return (await lint(documents, opts)).results;
}

describe('Linter service', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/services/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum OutputFormat {
TEAMCITY = 'teamcity',
PRETTY = 'pretty',
GITHUB_ACTIONS = 'github-actions',
SARIF = 'sarif',
}

export interface ILintConfig {
Expand Down
14 changes: 11 additions & 3 deletions packages/cli/src/services/linter/linter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* eslint-disable no-console */
import { Document, IRuleResult, Spectral } from '@stoplight/spectral-core';
import { Document, IRuleResult, Ruleset, Spectral } from '@stoplight/spectral-core';
import { readParsable, IFileReadOptions } from '@stoplight/spectral-runtime';
import * as Parsers from '@stoplight/spectral-parsers';
import { getRuleset, listFiles, segregateEntriesPerKind, readFileDescriptor } from './utils';
import { getResolver } from './utils/getResolver';
import { ILintConfig } from '../config';
import { CLIError } from '../../errors';

export async function lint(documents: Array<number | string>, flags: ILintConfig): Promise<IRuleResult[]> {
export interface LinterResult {
results: IRuleResult[];
resolvedRuleset: Ruleset;
}

export async function lint(documents: Array<number | string>, flags: ILintConfig): Promise<LinterResult> {
const spectral = new Spectral({
resolver: getResolver(flags.resolver),
});
Expand Down Expand Up @@ -48,7 +53,10 @@ export async function lint(documents: Array<number | string>, flags: ILintConfig
);
}

return results;
return {
results: results,
resolvedRuleset: ruleset,
};
}

const createDocument = async (
Expand Down
28 changes: 24 additions & 4 deletions packages/cli/src/services/output.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import * as process from 'process';
import { IRuleResult } from '@stoplight/spectral-core';
import { IRuleResult, Ruleset } from '@stoplight/spectral-core';
import { promises as fs } from 'fs';
import { html, json, junit, stylish, teamcity, text, pretty, githubActions } from '@stoplight/spectral-formatters';
import {
html,
json,
junit,
stylish,
teamcity,
text,
pretty,
githubActions,
sarif,
} from '@stoplight/spectral-formatters';
import type { Formatter, FormatterOptions } from '@stoplight/spectral-formatters';
import type { OutputFormat } from './config';
import { VERSION } from '../version';

const formatters: Record<OutputFormat, Formatter> = {
json,
Expand All @@ -14,10 +25,19 @@ const formatters: Record<OutputFormat, Formatter> = {
text,
teamcity,
'github-actions': githubActions,
sarif,
};

export function formatOutput(results: IRuleResult[], format: OutputFormat, formatOptions: FormatterOptions): string {
return formatters[format](results, formatOptions);
export function formatOutput(
results: IRuleResult[],
format: OutputFormat,
formatOptions: FormatterOptions,
ruleset: Ruleset,
): string {
return formatters[format](results, formatOptions, {
ruleset,
spectralVersion: VERSION,
});
}

export async function writeOutput(outputStr: string, outputFile: string): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion test-harness/scenarios/help-no-document.scenario
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Options:
--version Show version number [boolean]
--help Show help [boolean]
-e, --encoding text encoding to use [string] [choices: "utf8", "ascii", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1"] [default: "utf8"]
-f, --format formatters to use for outputting results, more than one can be given joining them with a comma [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions"] [default: "stylish"]
-f, --format formatters to use for outputting results, more than one can be given joining them with a comma [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif"] [default: "stylish"]
-o, --output where to output results, can be a single file name, multiple "output.<format>" or missing to print to stdout [string]
--stdin-filepath path to a file to pretend that stdin comes from [string]
--resolver path to custom json-ref-resolver instance [string]
Expand Down
2 changes: 1 addition & 1 deletion test-harness/scenarios/strict-options.scenario
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Options:
--version Show version number [boolean]
--help Show help [boolean]
-e, --encoding text encoding to use [string] [choices: "utf8", "ascii", "utf-8", "utf16le", "ucs2", "ucs-2", "base64", "latin1"] [default: "utf8"]
-f, --format formatters to use for outputting results, more than one can be given joining them with a comma [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions"] [default: "stylish"]
-f, --format formatters to use for outputting results, more than one can be given joining them with a comma [string] [choices: "json", "stylish", "junit", "html", "text", "teamcity", "pretty", "github-actions", "sarif"] [default: "stylish"]
-o, --output where to output results, can be a single file name, multiple "output.<format>" or missing to print to stdout [string]
--stdin-filepath path to a file to pretend that stdin comes from [string]
--resolver path to custom json-ref-resolver instance [string]
Expand Down
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2657,7 +2657,7 @@ __metadata:
"@stoplight/json": ~3.21.0
"@stoplight/path": 1.3.2
"@stoplight/spectral-core": ^1.18.3
"@stoplight/spectral-formatters": ^1.2.0
"@stoplight/spectral-formatters": ^1.3.0
"@stoplight/spectral-parsers": ^1.0.3
"@stoplight/spectral-ref-resolver": ^1.0.4
"@stoplight/spectral-ruleset-bundler": ^1.5.2
Expand Down Expand Up @@ -2731,7 +2731,7 @@ __metadata:
languageName: unknown
linkType: soft

"@stoplight/spectral-formatters@^1.2.0, @stoplight/spectral-formatters@workspace:packages/formatters":
"@stoplight/spectral-formatters@^1.3.0, @stoplight/spectral-formatters@workspace:packages/formatters":
version: 0.0.0-use.local
resolution: "@stoplight/spectral-formatters@workspace:packages/formatters"
dependencies:
Expand Down

0 comments on commit 959a86a

Please sign in to comment.