Repository: stoplightio/spectral (ts)

Issue:
Of model-description and precompilation of rulesets
**Describe the bug**

The doc states `In your ruleset file you can add extends: "spectral:oas" and you'll get everything from spectral:oas2 and spectral:oas3.`

`model-description` is a rule that exists in two versions: one for oas2 and a different one (but named the same way) for oas3.

Precompiled `oas.json` (under `/rulesets/precompiled/` only contains one instance of `model-description` (which happens to be the oas3 version)

**To Reproduce**

```
$ rm -rf rulesets && yarn build
yarn run v1.15.2
$ copyfiles -u 1 "src/rulesets/oas*/**/*.json" dist && copyfiles -u 1 "src/rulesets/oas*/**/*.json" ./
$ tsc -p ./tsconfig.build.json
$ yarn build.oas-functions && yarn compile-rulesets
$ rollup -c

C:\spectral\dist\rulesets\oas\functions\oasOp2xxResponse.js → dist\rulesets\oas\functions\oasOp2xxResponse.js...
created dist\rulesets\oas\functions\oasOp2xxResponse.js in 3.7s

[...]

C:\spectral\dist\rulesets\oas\functions\refSiblings.js → dist\rulesets\oas\functions\refSiblings.js...
created dist\rulesets\oas\functions\refSiblings.js in 2.7s
$ copyfiles -u 1 "dist/rulesets/oas*/functions/*.js" ./
$ node ./scripts/compile-rulesets.js
Done in 54.43s.

$ cat rulesets/precompiled/oas.json | grep -o "model-description" | wc -l
1
```

**Expected behavior**
An oas ruleset than can be run against oas2 and oas3 specifications

**Screenshots**

![image](https://user-images.githubusercontent.com/92363/66638960-38ecf600-ec16-11e9-98dd-d8cdb7dc0e9f.png)

**Environment (remove any that are not applicable):**

`develop` branch

Pull request description:
fix(rule): workaround 'model-description' rule name collision
Fixes #670
**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No

This proposed change also contains a test to ensure this kind of name collision won't happen again in the future.

Patch:
diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md
index 4442a4cc..ec984e1d 100644
--- a/docs/reference/openapi-rules.md
+++ b/docs/reference/openapi-rules.md
@@ -409,7 +409,7 @@ Server URL should not have a trailing slash.
 
 **Recommended:** Yes
 
-### model-description
+### definition-description
 
 Definition `description` must be present and non-empty string.
 
@@ -476,7 +476,7 @@ servers:
 If this is going out to the world, maybe have production and a general sandbox people can play with.
 
 
-### model-description
+### components-schema-description
 
 Model `description` must be present and non-empty string.
 
diff --git a/src/rulesets/oas2/index.json b/src/rulesets/oas2/index.json
index 3c5d2f11..ac1dd146 100644
--- a/src/rulesets/oas2/index.json
+++ b/src/rulesets/oas2/index.json
@@ -63,7 +63,7 @@
         }
       }
     },
-    "model-description": {
+    "definition-description": {
       "description": "Definition `description` must be present and non-empty string.",
       "recommended": false,
       "type": "style",
diff --git a/src/rulesets/oas3/index.json b/src/rulesets/oas3/index.json
index fa1e6f85..75a2775f 100644
--- a/src/rulesets/oas3/index.json
+++ b/src/rulesets/oas3/index.json
@@ -24,7 +24,7 @@
         "api"
       ]
     },
-    "model-description": {
+    "components-schema-description": {
       "description": "Model `description` must be present and non-empty string.",
       "recommended": false,
       "type": "style",


Test patch:
diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts
index 36b6c651..f903c1bf 100644
--- a/src/__tests__/linter.test.ts
+++ b/src/__tests__/linter.test.ts
@@ -150,7 +150,7 @@ describe('linter', () => {
     const { rules: oas3Rules } = await readRuleset('spectral:oas3');
     spectral.setRules(mergeRules(oas3Rules, {
       'valid-example-in-schemas': 'off',
-      'model-description': -1,
+      'components-schema-description': -1,
     }) as RuleCollection);
 
     const result = await spectral.run(invalidSchema);
diff --git a/src/rulesets/__tests__/rule_name_conflicts.jest.test.ts b/src/rulesets/__tests__/rule_name_conflicts.jest.test.ts
new file mode 100644
index 00000000..0615c86d
--- /dev/null
+++ b/src/rulesets/__tests__/rule_name_conflicts.jest.test.ts
@@ -0,0 +1,50 @@
+import * as fs from 'fs';
+
+describe('Rule name conflicts among rulesets', () => {
+  test.each([['oas', 'oas2'], ['oas2', 'oas3'], ['oas3', 'oas']])(
+    'do not exist (comparing %s with %s)',
+    (one, another) => {
+      const oneContent = JSON.parse(fs.readFileSync(`${__dirname}/../${one}/index.json`, { encoding: 'utf8' }));
+      const anotherContent = JSON.parse(fs.readFileSync(`${__dirname}/../${another}/index.json`, { encoding: 'utf8' }));
+
+      interface IDescriptor {
+        name: string;
+        source: string;
+      }
+
+      const consolidated: Record<string, IDescriptor> = {};
+
+      const consolidate = (content: string, source: string) => {
+        Object.keys(content).forEach(key => {
+          consolidated[`${key}-${source}`] = { name: key, source };
+        });
+      };
+
+      consolidate(oneContent.rules, one);
+      consolidate(anotherContent.rules, another);
+
+      const sorted: Record<string, IDescriptor> = Object.keys(consolidated)
+        .sort()
+        .reduce(
+          (acc, key) => ({
+            ...acc,
+            [key]: consolidated[key],
+          }),
+          {},
+        );
+
+      const keys = Object.keys(sorted);
+      for (let i = 0; i < keys.length - 1; i++) {
+        const current = sorted[keys[i]];
+        const next = sorted[keys[i + 1]];
+        if (current.name !== next.name) {
+          continue;
+        }
+
+        throw new Error(`Rule name collision detected
+- ${current.name} (${current.source})
+- ${next.name} (${next.source})`);
+      }
+    },
+  );
+});
diff --git a/src/rulesets/oas2/__tests__/model-description.ts b/src/rulesets/oas2/__tests__/model-description.ts
index 5ee2de52..4156674b 100644
--- a/src/rulesets/oas2/__tests__/model-description.ts
+++ b/src/rulesets/oas2/__tests__/model-description.ts
@@ -2,12 +2,12 @@ import { DiagnosticSeverity } from '@stoplight/types';
 import { RuleType, Spectral } from '../../../spectral';
 import * as ruleset from '../index.json';
 
-describe('model-description', () => {
+describe('definition-description', () => {
   const s = new Spectral();
   s.setRules({
-    'model-description': Object.assign(ruleset.rules['model-description'], {
+    'definition-description': Object.assign(ruleset.rules['definition-description'], {
       recommended: true,
-      type: RuleType[ruleset.rules['model-description'].type],
+      type: RuleType[ruleset.rules['definition-description'].type],
     }),
   });
 
@@ -34,7 +34,7 @@ describe('model-description', () => {
     });
     expect(results).toEqual([
       {
-        code: 'model-description',
+        code: 'definition-description',
         message: 'Definition `description` must be present and non-empty string.',
         path: ['definitions', 'user'],
         range: {
diff --git a/src/rulesets/oas3/__tests__/model-description.ts b/src/rulesets/oas3/__tests__/model-description.ts
index 629c5de8..becbcb67 100644
--- a/src/rulesets/oas3/__tests__/model-description.ts
+++ b/src/rulesets/oas3/__tests__/model-description.ts
@@ -2,12 +2,12 @@ import { DiagnosticSeverity } from '@stoplight/types';
 import { RuleType, Spectral } from '../../../spectral';
 import * as ruleset from '../index.json';
 
-describe('model-description', () => {
+describe('components-schema-description', () => {
   const s = new Spectral();
   s.setRules({
-    'model-description': Object.assign(ruleset.rules['model-description'], {
+    'components-schema-description': Object.assign(ruleset.rules['components-schema-description'], {
       recommended: true,
-      type: RuleType[ruleset.rules['model-description'].type],
+      type: RuleType[ruleset.rules['components-schema-description'].type],
     }),
   });
 
@@ -38,7 +38,7 @@ describe('model-description', () => {
     });
     expect(results).toEqual([
       {
-        code: 'model-description',
+        code: 'components-schema-description',
         message: 'Model `description` must be present and non-empty string.',
         path: ['components', 'schemas', 'user'],
         range: {
diff --git a/test-harness/scenarios/enabled-rules-amount.oas3.scenario b/test-harness/scenarios/enabled-rules-amount.oas3.scenario
index 8052a526..adb3adf2 100644
--- a/test-harness/scenarios/enabled-rules-amount.oas3.scenario
+++ b/test-harness/scenarios/enabled-rules-amount.oas3.scenario
@@ -24,7 +24,7 @@ components:
 ====command====
 lint {document} --ruleset ./test-harness/scenarios/rulesets/parameter-description.oas3.yaml -v
 ====stdout====
-Found 52 rules (1 enabled)
+Found 53 rules (1 enabled)
 Linting {document}
 OpenAPI 3.x detected