diff --git a/src/cli/services/linter/linter.ts b/src/cli/services/linter/linter.ts index 0520072bd..45c0a21b4 100644 --- a/src/cli/services/linter/linter.ts +++ b/src/cli/services/linter/linter.ts @@ -17,7 +17,7 @@ import { isRuleEnabled } from '../../../runner'; import { IRuleResult, Spectral } from '../../../spectral'; import { FormatLookup, IParsedResult } from '../../../types'; import { ILintConfig } from '../../../types/config'; -import { getRuleset, listFiles, skipRules } from './utils'; +import { deduplicateResults, getRuleset, listFiles, skipRules } from './utils'; const KNOWN_FORMATS: Array<[string, FormatLookup, string]> = [ ['oas2', isOpenApiv2, 'OpenAPI 2.0 (Swagger) detected'], @@ -91,5 +91,5 @@ export async function lint(documents: string[], flags: ILintConfig) { ); } - return results; + return deduplicateResults(results); } diff --git a/src/cli/services/linter/utils/__tests__/__fixtures__/duplicate-validation-results.json b/src/cli/services/linter/utils/__tests__/__fixtures__/duplicate-validation-results.json new file mode 100644 index 000000000..a436eb98d --- /dev/null +++ b/src/cli/services/linter/utils/__tests__/__fixtures__/duplicate-validation-results.json @@ -0,0 +1,105 @@ +[ + { + "code": "valid-schema-example-in-content", + "message": "\"schema.example\" property type should be integer", + "path": [ + "paths", + "/resource", + "get", + "responses", + "200", + "content", + "application/json", + "schema", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + }, + { + "code": "valid-schema-example-in-content", + "message": "\"schema.example\" property type should be integer", + "path": [ + "paths", + "/resource", + "get", + "responses", + "200", + "content", + "application/json", + "schema", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + }, + { + "code": "valid-schema-example-in-content", + "message": "\"schema.example\" property type should be integer", + "path": [ + "paths", + "/resource", + "get", + "responses", + "200", + "content", + "application/json", + "schema", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + }, + { + "code": "valid-example-in-schemas", + "message": "\"Test.example\" property type should be integer", + "path": [ + "components", + "schemas", + "Test", + "example" + ], + "severity": 0, + "source": "/home/Spectral/test-harness/scenarios/documents/invalid-schema-example/lib.yaml", + "range": { + "start": { + "line": 20, + "character": 15 + }, + "end": { + "line": 20, + "character": 19 + } + } + } +] diff --git a/src/cli/services/linter/utils/__tests__/deduplicateResults.spec.ts b/src/cli/services/linter/utils/__tests__/deduplicateResults.spec.ts new file mode 100644 index 000000000..b0cd2ee18 --- /dev/null +++ b/src/cli/services/linter/utils/__tests__/deduplicateResults.spec.ts @@ -0,0 +1,32 @@ +import { deduplicateResults } from '../deduplicateResults'; + +import * as duplicateValidationResults from './__fixtures__/duplicate-validation-results.json'; + +describe('deduplicateResults util', () => { + it('deduplicate exact validation results', () => { + expect(deduplicateResults(duplicateValidationResults)).toEqual([ + expect.objectContaining({ + code: 'valid-schema-example-in-content', + }), + expect.objectContaining({ + code: 'valid-example-in-schemas', + }), + ]); + }); + + it('deduplicate exact validation results with unknown source', () => { + const duplicateValidationResultsWithNoSource = duplicateValidationResults.map(result => ({ + ...result, + source: void 0, + })); + + expect(deduplicateResults(duplicateValidationResultsWithNoSource)).toEqual([ + expect.objectContaining({ + code: 'valid-schema-example-in-content', + }), + expect.objectContaining({ + code: 'valid-example-in-schemas', + }), + ]); + }); +}); diff --git a/src/cli/services/linter/utils/deduplicateResults.ts b/src/cli/services/linter/utils/deduplicateResults.ts new file mode 100644 index 000000000..ca275a3f3 --- /dev/null +++ b/src/cli/services/linter/utils/deduplicateResults.ts @@ -0,0 +1,26 @@ +import { IRange } from '@stoplight/types/dist'; +import { IRuleResult } from '../../../../types'; + +type Dictionary = { [key in K]: T }; + +const ARTIFICIAL_ROOT = Symbol('root'); + +const serializeRange = ({ start, end }: IRange) => `${start.line}:${start.character}:${end.line}:${end.character}`; +const getIdentifier = (result: IRuleResult) => `${result.path.join('/')}${result.code}${serializeRange(result.range)}`; + +export const deduplicateResults = (results: IRuleResult[]) => { + const seen: Dictionary, symbol> = {}; + + return results.filter(result => { + const source = result.source === void 0 ? ARTIFICIAL_ROOT : result.source; + const identifier = getIdentifier(result); + if (!(source in seen)) { + seen[source] = {}; + } else if (identifier in seen[source]) { + return false; + } + + seen[source][identifier] = true; + return true; + }); +}; diff --git a/src/cli/services/linter/utils/index.ts b/src/cli/services/linter/utils/index.ts index 4912e9c0c..64d27e7d0 100644 --- a/src/cli/services/linter/utils/index.ts +++ b/src/cli/services/linter/utils/index.ts @@ -1,3 +1,4 @@ +export * from './deduplicateResults'; export * from './getRuleset'; export * from './listFiles'; export * from './skipRules'; diff --git a/src/runner.ts b/src/runner.ts index f3d64e02d..508591f14 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -14,7 +14,7 @@ export const runRules = ( rules: RunRuleCollection, functions: FunctionCollection, ): IRuleResult[] => { - let results: IRuleResult[] = []; + const results: IRuleResult[] = []; for (const name in rules) { if (!rules.hasOwnProperty(name)) continue; @@ -34,7 +34,7 @@ export const runRules = ( } try { - results = results.concat(runRule(resolved, rule, functions)); + results.push(...runRule(resolved, rule, functions)); } catch (e) { console.error(`Unable to run rule '${name}':\n${e}`); }