diff --git a/.changeset/tasty-cougars-dress.md b/.changeset/tasty-cougars-dress.md new file mode 100644 index 0000000000..1bf1b22f94 --- /dev/null +++ b/.changeset/tasty-cougars-dress.md @@ -0,0 +1,6 @@ +--- +"@redocly/openapi-core": patch +"@redocly/cli": patch +--- + +Updated `operation-tag-defined` built-in rule to verify tags are defined on the operation prior to matching them to a global tag. diff --git a/__tests__/lint/turn-on-all-rules/snapshot.js b/__tests__/lint/turn-on-all-rules/snapshot.js index 76f7882892..e436adf919 100644 --- a/__tests__/lint/turn-on-all-rules/snapshot.js +++ b/__tests__/lint/turn-on-all-rules/snapshot.js @@ -55,7 +55,21 @@ Operation object should contain \`operationId\` field. Error was generated by the operation-operationId rule. -[5] openapi.yaml:9:5 at #/paths/~1ping~1{id}~1{test}/get/summary +[5] openapi.yaml:9:5 at #/paths/~1ping~1{id}~1{test}/get + +Operation tags should be defined + + 7 | paths: + 8 | '/ping/{id}/{test}': + 9 | get: + | ^^^ +10 | parameters: +11 | - in: path + +Error was generated by the operation-tag-defined rule. + + +[6] openapi.yaml:9:5 at #/paths/~1ping~1{id}~1{test}/get/summary Operation object should contain \`summary\` field. @@ -69,7 +83,7 @@ Operation object should contain \`summary\` field. Error was generated by the operation-summary rule. -[6] openapi.yaml:9:5 at #/paths/~1ping~1{id}~1{test}/get/description +[7] openapi.yaml:9:5 at #/paths/~1ping~1{id}~1{test}/get/description Operation object should contain \`description\` field. @@ -83,7 +97,7 @@ Operation object should contain \`description\` field. Error was generated by the operation-description rule. -[7] openapi.yaml:12:17 at #/paths/~1ping~1{id}~1{test}/get/parameters/0/name +[8] openapi.yaml:12:17 at #/paths/~1ping~1{id}~1{test}/get/parameters/0/name Path parameter \`test_id\` is not used in the path \`/ping/{id}/{test}\`. @@ -97,7 +111,7 @@ Path parameter \`test_id\` is not used in the path \`/ping/{id}/{test}\`. Error was generated by the path-parameters-defined rule. -[8] openapi.yaml:12:17 at #/paths/~1ping~1{id}~1{test}/get/parameters/0/name +[9] openapi.yaml:12:17 at #/paths/~1ping~1{id}~1{test}/get/parameters/0/name Path parameter \`test_id\` is not used in the path \`/ping/{id}/{test}\`. @@ -111,7 +125,7 @@ Path parameter \`test_id\` is not used in the path \`/ping/{id}/{test}\`. Error was generated by the path-params-defined rule. -[9] openapi.yaml:17:7 at #/paths/~1ping~1{id}~1{test}/get/responses +[10] openapi.yaml:17:7 at #/paths/~1ping~1{id}~1{test}/get/responses Operation must have at least one \`4XX\` response. @@ -125,7 +139,7 @@ Operation must have at least one \`4XX\` response. Error was generated by the operation-4xx-response rule. -[10] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters +[11] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters The operation does not define the path parameter \`{id}\` expected by path \`/ping/{id}/{test}\`. @@ -139,7 +153,7 @@ The operation does not define the path parameter \`{id}\` expected by path \`/pi Error was generated by the path-parameters-defined rule. -[11] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters +[12] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters The operation does not define the path parameter \`{test}\` expected by path \`/ping/{id}/{test}\`. @@ -153,7 +167,7 @@ The operation does not define the path parameter \`{test}\` expected by path \`/ Error was generated by the path-parameters-defined rule. -[12] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters +[13] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters The operation does not define the path parameter \`{id}\` expected by path \`/ping/{id}/{test}\`. @@ -167,7 +181,7 @@ The operation does not define the path parameter \`{id}\` expected by path \`/pi Error was generated by the path-params-defined rule. -[13] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters +[14] openapi.yaml:10:7 at #/paths/~1ping~1{id}~1{test}/get/parameters The operation does not define the path parameter \`{test}\` expected by path \`/ping/{id}/{test}\`. @@ -181,7 +195,7 @@ The operation does not define the path parameter \`{test}\` expected by path \`/ Error was generated by the path-params-defined rule. -[14] openapi.yaml:8:3 at #/paths/~1ping~1{id}~1{test} +[15] openapi.yaml:8:3 at #/paths/~1ping~1{id}~1{test} path segment \`ping\` should be plural. @@ -197,7 +211,7 @@ Error was generated by the path-segment-plural rule. openapi.yaml: validated in ms -❌ Validation failed with 14 errors. +❌ Validation failed with 15 errors. run \`redocly lint --generate-ignore-file\` to add all problems to the ignore file. diff --git a/docs/rules/oas/operation-tag-defined.md b/docs/rules/oas/operation-tag-defined.md index 1a16356a25..097759b72f 100644 --- a/docs/rules/oas/operation-tag-defined.md +++ b/docs/rules/oas/operation-tag-defined.md @@ -16,9 +16,9 @@ Disallows use of tags in operations that aren't globally defined. OpenAPI tags can be used for different purposes. Tags are declared in the root of the OpenAPI description. -Then, they are used in operations. +Then, they are used in operations. They are recommmended for grouping common operations within your api description. -This rule says that if an operation uses a tag, it must be defined in the root tags declaration. +This rule first checks if a tag exists on the operation. Subsequently, if an operation uses a tag, it must be defined in the root `tags` declaration. This rule helps prevent typos and tag explosion. ## Configuration diff --git a/packages/core/src/rules/common/__tests__/operation-tag-defined.test.ts b/packages/core/src/rules/common/__tests__/operation-tag-defined.test.ts new file mode 100644 index 0000000000..75ba28f230 --- /dev/null +++ b/packages/core/src/rules/common/__tests__/operation-tag-defined.test.ts @@ -0,0 +1,68 @@ +import { outdent } from 'outdent'; +import { lintDocument } from '../../../lint'; +import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils'; +import { BaseResolver } from '../../../resolve'; + +describe('Oas3 operation-tag-defined', () => { + it('should not report on operation object if at least one tag is defined', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.0.4 + tags: + - name: a + paths: + /some: + get: + tags: + - a + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ rules: { 'operation-tag-defined': 'error' } }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should report on operation object if no tags are defined', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.0.4 + tags: + - name: a + paths: + /some: + get: + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ rules: { 'operation-tag-defined': 'error' } }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/paths/~1some/get", + "reportOnKey": true, + "source": "foobar.yaml", + }, + ], + "message": "Operation tags should be defined", + "ruleId": "operation-tag-defined", + "severity": "error", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/packages/core/src/rules/common/operation-tag-defined.ts b/packages/core/src/rules/common/operation-tag-defined.ts index 804381aea0..fb382230de 100644 --- a/packages/core/src/rules/common/operation-tag-defined.ts +++ b/packages/core/src/rules/common/operation-tag-defined.ts @@ -11,7 +11,7 @@ export const OperationTagDefined: Oas3Rule | Oas2Rule = () => { definedTags = new Set((root.tags ?? []).map((t) => t.name)); }, Operation(operation: Oas2Operation | Oas3Operation, { report, location }: UserContext) { - if (operation.tags) { + if (operation?.tags) { for (let i = 0; i < operation.tags.length; i++) { if (!definedTags.has(operation.tags[i])) { report({ @@ -20,6 +20,11 @@ export const OperationTagDefined: Oas3Rule | Oas2Rule = () => { }); } } + } else { + report({ + message: `Operation tags should be defined`, + location: location.key(), + }); } }, };