From 6230858418194d9f8bb0cf60705caaa89a1e9c2b Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Mon, 1 Apr 2024 09:04:15 -0500 Subject: [PATCH] Replace oas2-unused-definition with az-unused-definition --- functions/unused-definition.js | 44 +++++++++ spectral.yaml | 11 +++ test/unused-definition.test.js | 170 +++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 functions/unused-definition.js create mode 100644 test/unused-definition.test.js diff --git a/functions/unused-definition.js b/functions/unused-definition.js new file mode 100644 index 0000000000..d57e3df402 --- /dev/null +++ b/functions/unused-definition.js @@ -0,0 +1,44 @@ +// Check all definitions in the document to see if they are used +// Use the spectral unreferencedReusableObject to find its list of unused definitions, +// and then remove any that `allOf` a used schema. + +const { unreferencedReusableObject } = require('@stoplight/spectral-functions'); + +const isObject = (obj) => obj && typeof obj === 'object'; + +// given should point to the member holding the potential reusable objects. +module.exports = (given, _, context) => { + if (!isObject(given)) { + return []; + } + const opts = { + reusableObjectsLocation: '#/definitions', + }; + const unreferencedDefinitionErrors = unreferencedReusableObject(given, opts, context); + + const unusedDefinitions = unreferencedDefinitionErrors.map((error) => error.path[1]); + + const allOfsUsedSchema = (schemaName) => { + const schema = given[schemaName]; + if (!isObject(schema) || !Array.isArray(schema.allOf)) { + return false; + } + + return schema.allOf.some((subSchema) => { + if (!isObject(subSchema) || !subSchema.$ref) { + return false; + } + + const reffedSchema = subSchema.$ref.split('/').pop(); + if (unusedDefinitions.includes(reffedSchema)) { + return false; + } + + return true; + }); + }; + + return unreferencedDefinitionErrors.filter( + (error) => !allOfsUsedSchema(error.path[1]), + ); +}; diff --git a/spectral.yaml b/spectral.yaml index deadd89b98..5b4b7726ef 100644 --- a/spectral.yaml +++ b/spectral.yaml @@ -21,12 +21,14 @@ functions: - security-definitions - security-requirements - schema-type-and-format + - unused-definition - version-policy rules: info-contact: off no-$ref-siblings: off oas2-api-host: off oas2-api-schemes: off + oas2-unused-definition: off openapi-tags: off operation-description: off operation-tags: off @@ -695,6 +697,15 @@ rules: field: schema function: truthy + az-unused-definition: + description: Potentially unused definition has been detected. + severity: warn + formats: ['oas2'] + resolved: false + given: $.definitions + then: + function: unused-definition + az-version-convention: description: API version should be a date in YYYY-MM-DD format, optionally suffixed with '-preview'. severity: error diff --git a/test/unused-definition.test.js b/test/unused-definition.test.js new file mode 100644 index 0000000000..34e642329b --- /dev/null +++ b/test/unused-definition.test.js @@ -0,0 +1,170 @@ +const { linterForRule } = require('./utils'); + +let linter; + +beforeAll(async () => { + linter = await linterForRule('az-unused-definition'); + return linter; +}); + +test('az-unused-definition should find errors', () => { + const oasDoc = { + swagger: '2.0', + paths: { + '/test1': { + post: { + parameters: [ + { + in: 'body', + name: 'body', + schema: { + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'Success', + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + definitions: { + Model1: { + type: 'object', + allOf: [ + { + $ref: '#/definitions/Model3', + }, + ], + }, + Model2: { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + }, + Model3: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + }, + }, + }; + return linter.run(oasDoc).then((results) => { + expect(results.length).toBe(1); + // Note: Model3 is not flagged as unused because it is used in Model1, + // even though Model1 is not used. And the new logic now filters out the + // error for Model1 because it allOfs Model3. + expect(results[0].path.join('.')).toBe('definitions.Model2'); + }); +}); + +test('az-unused-definition should not find errors', () => { + const oasDoc = { + swagger: '2.0', + paths: { + '/test1': { + post: { + parameters: [ + { + in: 'body', + name: 'body', + schema: { + $ref: '#/definitions/Model1', + }, + }, + ], + responses: { + 200: { + description: 'Success', + schema: { + $ref: '#/definitions/Model2', + }, + }, + }, + }, + }, + '/test2': { + post: { + parameters: [ + { + in: 'body', + name: 'body', + schema: { + $ref: '#/definitions/Model4', + }, + }, + ], + responses: { + 200: { + description: 'Success', + schema: { + $ref: '#/definitions/Model3', + }, + }, + }, + }, + }, + }, + definitions: { + Model1: { + type: 'object', + allOf: [ + { + $ref: '#/definitions/Model3', + }, + ], + }, + Model2: { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + }, + Model3: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + }, + Model4: { + type: 'object', + properties: { + id: { + type: 'string', + }, + }, + }, + Model5: { + type: 'object', + properties: { + bar: { + type: 'string', + }, + }, + allOf: [ + { + $ref: '#/definitions/Model4', + }, + ], + }, + }, + }; + return linter.run(oasDoc).then((results) => { + expect(results.length).toBe(0); + }); +});