Repository: Redocly/redocly-cli
Pull request #1112 (2023-05-31 10:03:07)

feat: create rule for $ref validation
## What/Why/How?

Add new rule to restrict the use of $ref

## Reference

Closes: https://github.com/Redocly/redocly-cli/issues/682

## Testing

## Screenshots (optional)
![Знімок екрана 2023-05-31 о 13 02 12](https://github.com/Redocly/redocly-cli/assets/106662428/457199c3-cd05-4020-95e5-913dfd7624dc)

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [x] All new/updated code is covered with tests

## Security

- [x] Security impact of change has been considered
- [x] Code follows company security practices and guidelines


github-actions[bot]: | Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `redocly lint packages/core/src/benchmark/benches/rebilly.yaml` | 1.250 ± 0.021 | 1.216 | 1.285 | 1.00 |
| `redocly-next lint packages/core/src/benchmark/benches/rebilly.yaml` | 1.270 ± 0.036 | 1.226 | 1.361 | 1.02 ± 0.03 |

<!-- thollander/actions-comment-pull-request "hyperfine-comparison" -->
IgorKarpiuk: @tatomyr I believe we can use it for both

Patch:
diff --git a/packages/core/src/rules/common/spec-ref-validation.ts b/packages/core/src/rules/common/spec-ref-validation.ts
new file mode 100644
index 00000000..af3bc9f8
--- /dev/null
+++ b/packages/core/src/rules/common/spec-ref-validation.ts
@@ -0,0 +1,30 @@
+import { Oas2Rule, Oas3Rule } from '../../visitors';
+import { isRef } from '../../ref-utils';
+
+export const SpecRefValidation: Oas3Rule | Oas2Rule = () => {
+  const nodesToSkip = [
+    'Schema',
+    'Response',
+    'Parameter',
+    'RequestBody',
+    'Example',
+    'Header',
+    'SecurityScheme',
+    'Link',
+    'Callback',
+    'PathItem',
+  ];
+
+  return {
+    any(_node, { report, rawNode, rawLocation, type }) {
+      const shouldCheck = !nodesToSkip.includes(type.name);
+
+      if (shouldCheck && isRef(rawNode)) {
+        report({
+          message: 'Field $ref is not expected here.',
+          location: rawLocation.child('$ref').key(),
+        });
+      }
+    },
+  };
+};
diff --git a/packages/core/src/rules/oas2/index.ts b/packages/core/src/rules/oas2/index.ts
index e6e28294..b6cf8dbc 100644
--- a/packages/core/src/rules/oas2/index.ts
+++ b/packages/core/src/rules/oas2/index.ts
@@ -40,6 +40,7 @@ import { ResponseContainsHeader } from '../common/response-contains-header';
 import { ResponseContainsProperty } from './response-contains-property';
 import { ScalarPropertyMissingExample } from '../common/scalar-property-missing-example';
 import { RequiredStringPropertyMissingMinLength } from '../common/required-string-property-missing-min-length';
+import { SpecRefValidation } from '../common/spec-ref-validation';
 
 export const rules = {
   spec: OasSpec as Oas2Rule,
@@ -84,6 +85,7 @@ export const rules = {
   'response-contains-property': ResponseContainsProperty as Oas2Rule,
   'scalar-property-missing-example': ScalarPropertyMissingExample,
   'required-string-property-missing-min-length': RequiredStringPropertyMissingMinLength,
+  'spec-ref-validation': SpecRefValidation,
 };
 
 export const preprocessors = {};
diff --git a/packages/core/src/rules/oas3/index.ts b/packages/core/src/rules/oas3/index.ts
index f4a644fc..c9897667 100644
--- a/packages/core/src/rules/oas3/index.ts
+++ b/packages/core/src/rules/oas3/index.ts
@@ -50,6 +50,7 @@ import { ScalarPropertyMissingExample } from '../common/scalar-property-missing-
 import { SpecComponentsInvalidMapName } from './spec-components-invalid-map-name';
 import { Operation4xxProblemDetailsRfc7807 } from './operation-4xx-problem-details-rfc7807';
 import { RequiredStringPropertyMissingMinLength } from '../common/required-string-property-missing-min-length';
+import { SpecRefValidation } from '../common/spec-ref-validation';
 
 export const rules = {
   spec: OasSpec,
@@ -104,6 +105,7 @@ export const rules = {
   'scalar-property-missing-example': ScalarPropertyMissingExample,
   'spec-components-invalid-map-name': SpecComponentsInvalidMapName,
   'required-string-property-missing-min-length': RequiredStringPropertyMissingMinLength,
+  'spec-ref-validation': SpecRefValidation,
 } as Oas3RuleSet;
 
 export const preprocessors = {};
diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts
index ad26721d..ddad57b9 100644
--- a/packages/core/src/types/redocly-yaml.ts
+++ b/packages/core/src/types/redocly-yaml.ts
@@ -54,6 +54,7 @@ const builtInRulesList = [
   'scalar-property-missing-example',
   'spec-components-invalid-map-name',
   'required-string-property-missing-min-length',
+  'spec-ref-validation',
 ];
 const nodeTypesList = [
   'any',


Test patch:
diff --git a/__tests__/lint/spec-ref-validation/info.yaml b/__tests__/lint/spec-ref-validation/info.yaml
new file mode 100644
index 00000000..80209c62
--- /dev/null
+++ b/__tests__/lint/spec-ref-validation/info.yaml
@@ -0,0 +1,11 @@
+description: |
+  This is a sample server Petstore server.
+version: 1.0.0
+title: Swagger Petstore
+contact:
+  name: API Support
+  email: apiteam@swagger.io
+  url: https://github.com/Redocly/redoc
+license:
+  name: Apache 2.0
+  url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
diff --git a/__tests__/lint/spec-ref-validation/openapi.yaml b/__tests__/lint/spec-ref-validation/openapi.yaml
new file mode 100644
index 00000000..b73390ec
--- /dev/null
+++ b/__tests__/lint/spec-ref-validation/openapi.yaml
@@ -0,0 +1,22 @@
+openapi: 3.1.0
+servers:
+  - url: //petstore.swagger.io/v2
+    description: Default server
+info:
+  $ref: './info.yaml'
+paths:
+  /pet:
+    get:
+      operationId: example
+      summary: summary example
+      responses:
+        '200':
+          description: example description
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  $ref: './props.yaml'
+                  name:
+                    type: string
diff --git a/__tests__/lint/spec-ref-validation/props.yaml b/__tests__/lint/spec-ref-validation/props.yaml
new file mode 100644
index 00000000..7b78c5dc
--- /dev/null
+++ b/__tests__/lint/spec-ref-validation/props.yaml
@@ -0,0 +1,8 @@
+total:
+  description: Total data number
+  type: number
+  example: 1000
+pages:
+  description: Total pages
+  type: number
+  example: 5
diff --git a/__tests__/lint/spec-ref-validation/redocly.yaml b/__tests__/lint/spec-ref-validation/redocly.yaml
new file mode 100644
index 00000000..ebe4d374
--- /dev/null
+++ b/__tests__/lint/spec-ref-validation/redocly.yaml
@@ -0,0 +1,6 @@
+apis:
+  main:
+    root: ./openapi.yaml
+
+rules:
+  spec-ref-validation: error
diff --git a/__tests__/lint/spec-ref-validation/snapshot.js b/__tests__/lint/spec-ref-validation/snapshot.js
new file mode 100644
index 00000000..af14460e
--- /dev/null
+++ b/__tests__/lint/spec-ref-validation/snapshot.js
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`E2E lint spec-ref-validation 1`] = `
+
+validating /openapi.yaml...
+[1] openapi.yaml:6:3 at #/info/$ref
+
+Field $ref is not expected here.
+
+4 |     description: Default server
+5 | info:
+6 |   $ref: './info.yaml'
+  |   ^^^^
+7 | paths:
+8 |   /pet:
+
+Error was generated by the spec-ref-validation rule.
+
+
+[2] openapi.yaml:20:19 at #/paths/~1pet/get/responses/200/content/application~1json/schema/properties/$ref
+
+Field $ref is not expected here.
+
+18 | type: object
+19 | properties:
+20 |   $ref: './props.yaml'
+   |   ^^^^
+21 |   name:
+22 |     type: string
+
+Error was generated by the spec-ref-validation rule.
+
+
+/openapi.yaml: validated in <test>ms
+
+❌ Validation failed with 2 errors.
+run \`redocly lint --generate-ignore-file\` to add all problems to the ignore file.
+
+
+`;
diff --git a/packages/core/src/rules/common/__tests__/spec-ref-validation.test.ts b/packages/core/src/rules/common/__tests__/spec-ref-validation.test.ts
new file mode 100644
index 00000000..7b6d3c89
--- /dev/null
+++ b/packages/core/src/rules/common/__tests__/spec-ref-validation.test.ts
@@ -0,0 +1,69 @@
+import { makeConfig, parseYamlToDocument, replaceSourceWithRef } from '../../../../__tests__/utils';
+import { outdent } from 'outdent';
+import { lintDocument } from '../../../lint';
+import { BaseResolver } from '../../../resolve';
+
+describe('Oas3 spec-ref-validation', () => {
+  it('should report about invalid usage of $ref', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+      openapi: "3.1.0"
+      info:
+        $ref: '#/components/schemas/test'
+      paths:
+        /store/subscribe:
+          post:
+            responses:
+              '201':
+                content:
+                  application/json:
+                    schema:
+                      type: object
+                      properties:
+                        $ref: '#/components/schemas/test'
+                        subscriptionId:
+                          type: string
+                          example: AAA-123-BBB-456
+      components:
+        schemas:
+          test:
+            type: object                    
+		`
+    );
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'spec-ref-validation': 'error' }),
+    });
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "location": Array [
+            Object {
+              "pointer": "#/info/$ref",
+              "reportOnKey": true,
+              "source": "",
+            },
+          ],
+          "message": "Field $ref is not expected here.",
+          "ruleId": "spec-ref-validation",
+          "severity": "error",
+          "suggest": Array [],
+        },
+        Object {
+          "location": Array [
+            Object {
+              "pointer": "#/paths/~1store~1subscribe/post/responses/201/content/application~1json/schema/properties/$ref",
+              "reportOnKey": true,
+              "source": "",
+            },
+          ],
+          "message": "Field $ref is not expected here.",
+          "ruleId": "spec-ref-validation",
+          "severity": "error",
+          "suggest": Array [],
+        },
+      ]
+    `);
+  });
+});