Repository: stoplightio/spectral
Pull request #681 (2019-10-14 22:39:53)

fix(cli): do not return duplicate results
Fixes https://github.com/stoplightio/spectral/issues/680

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No




Patch:
diff --git a/src/cli/services/linter/linter.ts b/src/cli/services/linter/linter.ts
index 0520072b..45c0a21b 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/deduplicateResults.ts b/src/cli/services/linter/utils/deduplicateResults.ts
new file mode 100644
index 00000000..ca275a3f
--- /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<T, K extends PropertyKey> = { [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<Dictionary<string, string>, 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 4912e9c0..64d27e7d 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 f3d64e02..508591f1 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}`);
     }


Test patch:
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 00000000..a436eb98
--- /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 00000000..b0cd2ee1
--- /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',
+      }),
+    ]);
+  });
+});