diff --git a/docs/reference/openapi-rules.md b/docs/reference/openapi-rules.md index ef3f65974..4f6e3e6b4 100644 --- a/docs/reference/openapi-rules.md +++ b/docs/reference/openapi-rules.md @@ -576,9 +576,9 @@ servers: - url: https://example.com/api/ ``` -### oas3-unused-components-schema +### oas3-unused-component -Potential unused reusable `schema` entry has been detected. +Potential unused reusable `components` entry has been detected. _Warning:_ This rule may identify false positives when linting a specification that acts as a library (a container storing reusable objects, leveraged by other diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts index 16212a76c..d20d85df4 100644 --- a/src/__tests__/linter.test.ts +++ b/src/__tests__/linter.test.ts @@ -715,13 +715,13 @@ responses:: !!foo code: 'invalid-ref', }), expect.objectContaining({ - code: 'oas3-unused-components-schema', - message: 'Potentially unused components schema has been detected.', + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', path: ['components', 'schemas', 'Pets'], }), expect.objectContaining({ - code: 'oas3-unused-components-schema', - message: 'Potentially unused components schema has been detected.', + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', path: ['components', 'schemas', 'foo'], }), expect.objectContaining({ diff --git a/src/cli/services/__tests__/linter.test.ts b/src/cli/services/__tests__/linter.test.ts index 2e03c9b7b..212e71a8b 100644 --- a/src/cli/services/__tests__/linter.test.ts +++ b/src/cli/services/__tests__/linter.test.ts @@ -391,7 +391,7 @@ describe('Linter service', () => { source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), }), expect.objectContaining({ - code: 'oas3-unused-components-schema', + code: 'oas3-unused-component', path: ['components', 'schemas', 'Pets'], source: join(process.cwd(), 'src/__tests__/__fixtures__/petstore.invalid-schema.oas3.json'), }), diff --git a/src/rulesets/__tests__/__fixtures__/unusedComponent.json b/src/rulesets/__tests__/__fixtures__/unusedComponent.json new file mode 100644 index 000000000..d6193df2b --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedComponent.json @@ -0,0 +1,142 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Used Components", + "description": "Negative test of unused components", + "contact": { + "email": "anywho@widgetco.com" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v2" + } + ], + "tags": [ + { + "name": "pets" + } + ], + "paths": { + "/pet": { + "post": { + "description": "Add a new pet to the store", + "summary": "Add pet", + "operationId": "add_pet", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "param1", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + }, + "default": { + "description": "Not success" + } + } + } + } + }, + "components": { + "schemas": { + "SomeSchema": { + "description": "Used schema", + "type": "string" + } + }, + "parameters": { + "SomeParameter": { + "description": "Used parameter", + "name": "foo", + "in": "query", + "schema": { + "type": "string" + } + } + }, + "requestBodies": { + "SomeBody": { + "description": "Used request body", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "callbacks": { + "SomeCallback": { + "{$request.query.queryUrl}": { + "post": { + "requestBody": { + "description": "Callback payload", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + }, + "examples": { + "SomeExample": { + "description": "Used example", + "value": "foo" + } + }, + "headers": { + "SomeHeader": { + "description": "Used header", + "schema": { + "type": "string" + } + } + }, + "links": { + "SomeLink": { + "description": "Used link", + "operationId": "add_pet", + "parameters": { + "pet_id": "$response.body#/id" + } + } + }, + "responses": { + "SomeResponse": { + "description": "Used response" + } + } + } +} diff --git a/src/rulesets/__tests__/__fixtures__/unusedComponent.negative.json b/src/rulesets/__tests__/__fixtures__/unusedComponent.negative.json new file mode 100644 index 000000000..970b454b7 --- /dev/null +++ b/src/rulesets/__tests__/__fixtures__/unusedComponent.negative.json @@ -0,0 +1,162 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Used Components", + "description": "Negative test of unused components", + "contact": { + "email": "anywho@widgetco.com" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v2" + } + ], + "tags": [ + { + "name": "pets" + } + ], + "paths": { + "/pet": { + "post": { + "description": "Add a new pet to the store", + "summary": "Add pet", + "operationId": "add_pet", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "param1", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SomeSchema" + } + }, + { + "$ref": "#/components/parameters/SomeParameter" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/SomeBody" + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "Some-Header": { + "$ref": "#/components/headers/SomeHeader" + } + }, + "content": { + "application/json": { + "examples": { + "AnExample": { + "$ref": "#/components/examples/SomeExample" + } + } + } + }, + "links": { + "TheLink": { + "$ref": "#/components/links/SomeLink" + } + } + }, + "default": { + "$ref": "#/components/responses/SomeResponse" + } + }, + "callbacks": { + "TheCallback": { + "$ref": "#/components/callbacks/SomeCallback" + } + } + } + } + }, + "components": { + "schemas": { + "SomeSchema": { + "description": "Used schema", + "type": "string" + } + }, + "parameters": { + "SomeParameter": { + "description": "Used parameter", + "name": "foo", + "in": "query", + "schema": { + "type": "string" + } + } + }, + "requestBodies": { + "SomeBody": { + "description": "Used request body", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "callbacks": { + "SomeCallback": { + "{$request.query.queryUrl}": { + "post": { + "requestBody": { + "description": "Callback payload", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + }, + "examples": { + "SomeExample": { + "description": "Used example", + "value": "foo" + } + }, + "headers": { + "SomeHeader": { + "description": "Used header", + "schema": { + "type": "string" + } + } + }, + "links": { + "SomeLink": { + "description": "Used link", + "operationId": "add_pet", + "parameters": { + "pet_id": "$response.body#/id" + } + } + }, + "responses": { + "SomeResponse": { + "description": "Used response" + } + } + } +} diff --git a/src/rulesets/oas/__tests__/oas3-unused-components-schema.jest.test.ts b/src/rulesets/oas/__tests__/oas3-unused-component.jest.test.ts similarity index 87% rename from src/rulesets/oas/__tests__/oas3-unused-components-schema.jest.test.ts rename to src/rulesets/oas/__tests__/oas3-unused-component.jest.test.ts index 474e8bf0d..f81b8f345 100644 --- a/src/rulesets/oas/__tests__/oas3-unused-components-schema.jest.test.ts +++ b/src/rulesets/oas/__tests__/oas3-unused-component.jest.test.ts @@ -4,19 +4,19 @@ import * as nock from 'nock'; import { Document } from '../../../document'; import { readParsable } from '../../../fs/reader'; -import type { Spectral } from '../../../index'; +import { Spectral } from '../../../index'; import * as Parsers from '../../../parsers'; import { createWithRules } from './__helpers__/createWithRules'; import { httpAndFileResolver } from '../../../resolvers/http-and-file'; -describe('unusedComponentsSchema - Http and fs remote references', () => { +describe('unusedComponent - Http and fs remote references', () => { let s: Spectral; beforeEach(async () => { - s = await createWithRules(['oas3-unused-components-schema'], { resolver: httpAndFileResolver }); + s = await createWithRules(['oas3-unused-component'], { resolver: httpAndFileResolver }); }); - describe('reports unreferenced components schemas', () => { + describe('reports unreferenced components', () => { test('when analyzing an in-memory document', async () => { nock('https://oas3.library.com') .get('/defs.json') @@ -84,8 +84,8 @@ describe('unusedComponentsSchema - Http and fs remote references', () => { expect(results).toEqual([ { - code: 'oas3-unused-components-schema', - message: 'Potentially unused components schema has been detected.', + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', path: ['components', 'schemas', 'Unhooked'], range: { end: { @@ -121,8 +121,8 @@ describe('unusedComponentsSchema - Http and fs remote references', () => { expect(results).toEqual([ { - code: 'oas3-unused-components-schema', - message: 'Potentially unused components schema has been detected.', + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', path: ['components', 'schemas', 'Unhooked'], range: { end: { diff --git a/src/rulesets/oas/__tests__/oas3-unused-components-schema.karma.test.ts b/src/rulesets/oas/__tests__/oas3-unused-component.karma.test.ts similarity index 83% rename from src/rulesets/oas/__tests__/oas3-unused-components-schema.karma.test.ts rename to src/rulesets/oas/__tests__/oas3-unused-component.karma.test.ts index 81b1c42d7..c8bf6a69e 100644 --- a/src/rulesets/oas/__tests__/oas3-unused-components-schema.karma.test.ts +++ b/src/rulesets/oas/__tests__/oas3-unused-component.karma.test.ts @@ -1,17 +1,17 @@ import { DiagnosticSeverity } from '@stoplight/types'; import { FetchMockSandbox } from 'fetch-mock'; import { Document } from '../../../document'; -import type { Spectral } from '../../../index'; +import { Spectral } from '../../../index'; import * as Parsers from '../../../parsers'; import { createWithRules } from './__helpers__/createWithRules'; import { httpAndFileResolver } from '../../../resolvers/http-and-file'; -describe('unusedComponentsSchema - Http remote references', () => { +describe('unusedComponent - Http remote references', () => { let fetchMock: FetchMockSandbox; let s: Spectral; beforeEach(async () => { - s = await createWithRules(['oas3-unused-components-schema'], { resolver: httpAndFileResolver }); + s = await createWithRules(['oas3-unused-component'], { resolver: httpAndFileResolver }); fetchMock = require('fetch-mock').default.sandbox(); window.fetch = fetchMock; }); @@ -20,7 +20,7 @@ describe('unusedComponentsSchema - Http remote references', () => { window.fetch = fetch; }); - test('reports unreferenced components schemas', async () => { + test('reports unreferenced components', async () => { fetchMock.mock('https://oas3.library.com/defs.json', { status: 200, body: { @@ -77,8 +77,8 @@ describe('unusedComponentsSchema - Http remote references', () => { expect(results).toEqual([ { - code: 'oas3-unused-components-schema', - message: 'Potentially unused components schema has been detected.', + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', path: ['components', 'schemas', 'Unhooked'], range: { end: { diff --git a/src/rulesets/oas/__tests__/oas3-unused-component.test.ts b/src/rulesets/oas/__tests__/oas3-unused-component.test.ts new file mode 100644 index 000000000..411918d35 --- /dev/null +++ b/src/rulesets/oas/__tests__/oas3-unused-component.test.ts @@ -0,0 +1,129 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import { Document } from '../../../document'; +import type { Spectral } from '../../../index'; +import * as Parsers from '../../../parsers'; +import { createWithRules } from './__helpers__/createWithRules'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; + +const noUnusedComponents = JSON.stringify(require('../../__tests__/__fixtures__/unusedComponent.negative.json')); +const unusedComponents = JSON.stringify(require('../../__tests__/__fixtures__/unusedComponent.json')); + +describe('oasUnusedComponent - Local references', () => { + let s: Spectral; + + beforeEach(async () => { + s = await createWithRules(['oas3-unused-component'], { resolver: httpAndFileResolver }); + }); + + test('does not report anything for empty object', async () => { + const results = await s.run({ + openapi: '3.0.0', + }); + + expect(results).toEqual([]); + }); + + test('does not throw when meeting an invalid json pointer', async () => { + const doc = `{ + "openapi": "3.0.0", + "x-hook": { + "$ref": "'$#@!!!' What?" + }, + "paths": { + }, + "components": { + "schemas": { + "NotHooked": { + "type": "object" + } + } + } + }`; + + const results = await s.run(doc); + + expect(results).toEqual([ + expect.objectContaining({ + code: 'invalid-ref', + path: ['x-hook', '$ref'], + }), + { + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'schemas', 'NotHooked'], + range: { + end: { + character: 28, + line: 10, + }, + start: { + character: 22, + line: 9, + }, + }, + severity: DiagnosticSeverity.Warning, + }, + ]); + }); + + test('does not report anything when all the components are referenced', async () => { + const results = await s.run(new Document(noUnusedComponents, Parsers.Json)); + + expect(results).toEqual([]); + }); + + test('reports orphaned components', async () => { + const results = await s.run(new Document(unusedComponents, Parsers.Json)); + + expect(results).toEqual([ + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'schemas', 'SomeSchema'], + severity: DiagnosticSeverity.Warning, + }), + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'parameters', 'SomeParameter'], + severity: DiagnosticSeverity.Warning, + }), + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'requestBodies', 'SomeBody'], + severity: DiagnosticSeverity.Warning, + }), + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'callbacks', 'SomeCallback'], + severity: DiagnosticSeverity.Warning, + }), + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'examples', 'SomeExample'], + severity: DiagnosticSeverity.Warning, + }), + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'headers', 'SomeHeader'], + severity: DiagnosticSeverity.Warning, + }), + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'links', 'SomeLink'], + severity: DiagnosticSeverity.Warning, + }), + expect.objectContaining({ + code: 'oas3-unused-component', + message: 'Potentially unused component has been detected.', + path: ['components', 'responses', 'SomeResponse'], + severity: DiagnosticSeverity.Warning, + }), + ]); + }); +}); diff --git a/src/rulesets/oas/__tests__/oas3-unused-components-schema.test.ts b/src/rulesets/oas/__tests__/oas3-unused-components-schema.test.ts deleted file mode 100644 index 09ccee0f9..000000000 --- a/src/rulesets/oas/__tests__/oas3-unused-components-schema.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { DiagnosticSeverity } from '@stoplight/types'; -import { Document } from '../../../document'; -import type { Spectral } from '../../../index'; -import * as Parsers from '../../../parsers'; -import { createWithRules } from './__helpers__/createWithRules'; -import { httpAndFileResolver } from '../../../resolvers/http-and-file'; - -describe('unusedComponentsSchema - Local references', () => { - let s: Spectral; - - beforeEach(async () => { - s = await createWithRules(['oas3-unused-components-schema'], { resolver: httpAndFileResolver }); - }); - - test('does not report anything for empty object', async () => { - const results = await s.run({ - openapi: '3.0.0', - }); - - expect(results).toEqual([]); - }); - - test('does not throw when meeting an invalid json pointer', async () => { - const doc = `{ - "openapi": "3.0.0", - "x-hook": { - "$ref": "'$#@!!!' What?" - }, - "paths": { - }, - "components": { - "schemas": { - "NotHooked": { - "type": "object" - } - } - } - }`; - - const results = await s.run(doc); - - expect(results).toEqual([ - expect.objectContaining({ - code: 'invalid-ref', - path: ['x-hook', '$ref'], - }), - { - code: 'oas3-unused-components-schema', - message: 'Potentially unused components schema has been detected.', - path: ['components', 'schemas', 'NotHooked'], - range: { - end: { - character: 28, - line: 10, - }, - start: { - character: 22, - line: 9, - }, - }, - severity: DiagnosticSeverity.Warning, - }, - ]); - }); - - test('does not report anything when all the components schemas are referenced', async () => { - const doc = `{ - "openapi": "3.0.0", - "x-hook": { - "$ref": "#/components/schemas/Hooked" - }, - "x-also-hook": { - "$ref": "#/components/schemas/Hooked" - }, - "paths": { - "/path": { - "post": { - "parameters": [ - { - "$ref": "#/components/schemas/HookedAsWell" - } - ] - } - } - }, - "components": { - "schemas": { - "Hooked": { - "type": "object" - }, - "HookedAsWell": { - "name": "value", - "in": "query", - "type": "number" - } - } - } - }`; - - const results = await s.run(new Document(doc, Parsers.Json)); - - expect(results).toEqual([]); - }); - - test('reports orphaned components schemas', async () => { - const doc = `{ - "openapi": "3.0.0", - "paths": { - "/path": { - "post": {} - } - }, - "components": { - "schemas": { - "BouhouhouIamUnused": { - "type": "object" - } - } - } - }`; - - const results = await s.run(new Document(doc, Parsers.Json)); - - expect(results).toEqual([ - { - code: 'oas3-unused-components-schema', - message: 'Potentially unused components schema has been detected.', - path: ['components', 'schemas', 'BouhouhouIamUnused'], - range: { - end: { - character: 11, - line: 11, - }, - start: { - character: 32, - line: 9, - }, - }, - severity: DiagnosticSeverity.Warning, - }, - ]); - }); -}); diff --git a/src/rulesets/oas/functions/oasUnusedComponent.ts b/src/rulesets/oas/functions/oasUnusedComponent.ts new file mode 100644 index 000000000..20f2b5b0c --- /dev/null +++ b/src/rulesets/oas/functions/oasUnusedComponent.ts @@ -0,0 +1,44 @@ +import type { IFunction, IFunctionContext, IFunctionResult } from '../../../types'; +import { isObject } from 'lodash'; + +export const oasUnusedComponent: IFunction<{}> = function ( + this: IFunctionContext, + targetVal, + opts, + _paths, + otherValues, +) { + const results: IFunctionResult[] = []; + + if (!isObject(targetVal.components)) { + return results; + } + + const componentTypes = [ + 'schemas', + 'responses', + 'parameters', + 'examples', + 'requestBodies', + 'headers', + 'links', + 'callbacks', + ]; + + for (const type of componentTypes) { + const resultsForType = this.functions.unreferencedReusableObject.call( + this, + targetVal.components[type], + { reusableObjectsLocation: `#/components/${type}` }, + _paths, + otherValues, + ); + if (resultsForType !== void 0 && Array.isArray(resultsForType)) { + results.push(...resultsForType); + } + } + + return results; +}; + +export default oasUnusedComponent; diff --git a/src/rulesets/oas/index.json b/src/rulesets/oas/index.json index 166f32cab..e5b905f5b 100644 --- a/src/rulesets/oas/index.json +++ b/src/rulesets/oas/index.json @@ -11,6 +11,7 @@ "oasOpSecurityDefined", "oasPathParam", "oasTagDefined", + "oasUnusedComponent", "typedEnum", "refSiblings" ], @@ -641,18 +642,15 @@ } } }, - "oas3-unused-components-schema": { - "description": "Potentially unused components schema has been detected.", + "oas3-unused-component": { + "description": "Potentially unused component has been detected.", "recommended": true, "formats": ["oas3"], "type": "style", "resolved": false, - "given": "$.components.schemas", + "given": "$", "then": { - "function": "unreferencedReusableObject", - "functionOptions": { - "reusableObjectsLocation": "#/components/schemas" - } + "function": "oasUnusedComponent" } } }