From a131408950613d6ae88c38f188fe10edc97e1d2d Mon Sep 17 00:00:00 2001 From: barrett-schonefeld <59850348+barrett-schonefeld@users.noreply.github.com> Date: Fri, 21 Feb 2020 12:35:10 -0600 Subject: [PATCH] feat: flag definitions that are defined but never used (#144) adds checks for parameters, responses, and all other components object types defined in the components section that are not used in the API definition --- .../2and3/semantic-validators/refs.js | 63 +++++-- test/plugins/validation/2and3/refs.js | 170 +++++++++++++++++- 2 files changed, 218 insertions(+), 15 deletions(-) diff --git a/src/plugins/validation/2and3/semantic-validators/refs.js b/src/plugins/validation/2and3/semantic-validators/refs.js index 2a1d1225f..d1fc1a2f7 100644 --- a/src/plugins/validation/2and3/semantic-validators/refs.js +++ b/src/plugins/validation/2and3/semantic-validators/refs.js @@ -15,12 +15,7 @@ module.exports.validate = function({ jsSpec, specStr, isOAS3 }) { return messages; } - const basePath = isOAS3 ? ['components', 'schemas'] : ['definitions']; - - // Assertation 1 - // This is a "creative" way to approach the problem of collecting used $refs, - // but other solutions required walking the jsSpec recursively to detect $refs, - // which can be quite slow. + // Collects all refs in the API definition const refRegex = /\$ref.*["'](.*)["']/g; let match = refRegex.exec(specStr); const refs = []; @@ -29,14 +24,58 @@ module.exports.validate = function({ jsSpec, specStr, isOAS3 }) { match = refRegex.exec(specStr); } + const definitionSectionName = isOAS3 ? 'components' : 'definitions'; // de-dupe the array, and filter out non-definition refs const definitionsRefs = filter(uniq(refs), v => - startsWith(v, `#/${basePath.join('/')}`) + startsWith(v, `#/${definitionSectionName}`) ); - const definitions = isOAS3 ? jsSpec.components.schemas : jsSpec.definitions; - each(definitions, (def, defName) => { - if (definitionsRefs.indexOf(`#/${basePath.join('/')}/${defName}`) === -1) { + // checks if the definitions are used, and if not, record a warning + if (isOAS3) { + // securitySchemes definition type excluded because + // security-definitions-ibm.js checks for unused security schemes + const definitionTypeList = [ + 'schemas', + 'parameters', + 'responses', + 'examples', + 'requestBodies', + 'headers', + 'links', + 'callbacks' + ]; + definitionTypeList.forEach(function(definitionType) { + if (jsSpec.components && jsSpec.components[definitionType]) { + recordDefinitionsNotUsed( + jsSpec.components[definitionType], + definitionsRefs, + ['components', definitionType], + messages + ); + } + }); + } else { + if (jsSpec.definitions) { + recordDefinitionsNotUsed( + jsSpec.definitions, + definitionsRefs, + ['definitions'], + messages + ); + } + } + + return messages; +}; + +function recordDefinitionsNotUsed( + definitions, + definitionsRefs, + basePath, + messages +) { + each(definitions, (_, defName) => { + if (!definitionsRefs.includes(`#/${basePath.join('/')}/${defName}`)) { messages.addMessage( `${basePath.join('.')}.${defName}`, 'Definition was declared but never used in document', @@ -44,6 +83,4 @@ module.exports.validate = function({ jsSpec, specStr, isOAS3 }) { ); } }); - - return messages; -}; +} diff --git a/test/plugins/validation/2and3/refs.js b/test/plugins/validation/2and3/refs.js index 8e63be400..088925b0a 100644 --- a/test/plugins/validation/2and3/refs.js +++ b/test/plugins/validation/2and3/refs.js @@ -43,7 +43,7 @@ describe('validation plugin - semantic - refs', function() { ); }); - it('should warn about an unused definition - OpenAPI 3', function() { + it('should warn about an unused schema definition - OpenAPI 3', function() { const jsSpec = { paths: { '/CoolPath/{id}': { @@ -83,7 +83,7 @@ describe('validation plugin - semantic - refs', function() { ); }); - it('should not warn about a used definition - OpenAPI 3', function() { + it('should not warn about a used schema definition - OpenAPI 3', function() { const jsSpec = { paths: { '/CoolPath/{id}': { @@ -124,4 +124,170 @@ describe('validation plugin - semantic - refs', function() { expect(res.warnings.length).toEqual(0); }); }); + + it('should warn about an unused parameter definition - OpenAPI 3', function() { + const jsSpec = { + paths: { + '/CoolPath/{id}': { + parameters: [ + { + $ref: '#/components/parameters/one_parameter_definition' + } + ] + } + }, + components: { + parameters: { + one_parameter_definition: { + name: 'id', + in: 'path', + description: 'An id' + }, + other_parameter_definition: { + name: 'other', + in: 'query', + description: 'another param' + } + } + } + }; + + const specStr = JSON.stringify(jsSpec, null, 2); + const isOAS3 = true; + + const res = validate({ jsSpec, specStr, isOAS3 }); + expect(res.warnings[0].path).toEqual( + 'components.parameters.other_parameter_definition' + ); + expect(res.warnings[0].message).toEqual( + 'Definition was declared but never used in document' + ); + expect(res.warnings.length).toEqual(1); + }); + + it('should not warn about used parameter definitions - OpenAPI 3', function() { + const jsSpec = { + paths: { + '/CoolPath/{id}': { + parameters: [ + { + $ref: '#/components/parameters/one_parameter_definition' + }, + { + $ref: '#/components/parameters/other_parameter_definition' + } + ] + } + }, + components: { + parameters: { + one_parameter_definition: { + name: 'id', + in: 'path', + description: 'An id' + }, + other_parameter_definition: { + name: 'other', + in: 'query', + description: 'another param' + } + } + } + }; + + const specStr = JSON.stringify(jsSpec, null, 2); + const isOAS3 = true; + + const res = validate({ jsSpec, specStr, isOAS3 }); + expect(res.warnings.length).toEqual(0); + }); + + it('should warn about an unused response definition - OpenAPI 3', function() { + const jsSpec = { + paths: { + '/CoolPath/{id}': { + responses: { + '200': { + description: '200 response', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + } + } + } + }, + components: { + responses: { + response400: { + description: '400 response', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + } + } + } + }; + + const specStr = JSON.stringify(jsSpec, null, 2); + const isOAS3 = true; + + const res = validate({ jsSpec, specStr, isOAS3 }); + expect(res.warnings[0].path).toEqual('components.responses.response400'); + expect(res.warnings[0].message).toEqual( + 'Definition was declared but never used in document' + ); + expect(res.warnings.length).toEqual(1); + }); + + it('should not warn about a used response definition - OpenAPI 3', function() { + const jsSpec = { + paths: { + '/CoolPath/{id}': { + responses: { + '200': { + description: '200 response', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + }, + '400': { + $ref: '#/components/responses/response400' + } + } + } + }, + components: { + responses: { + response400: { + description: '400 response', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + } + } + } + }; + + const specStr = JSON.stringify(jsSpec, null, 2); + const isOAS3 = true; + + const res = validate({ jsSpec, specStr, isOAS3 }); + expect(res.warnings.length).toEqual(0); + }); });