Repository: Redocly/redocly-cli
Pull request #1258 (2023-09-12 21:06:47)

Add `ignoreCase` for `tags-alphabetical` rule
## What/Why/How?

For human perception, case doesn't really make a huge difference, especially when trying to find an item on the list. We used to have a custom assertion to do the same thing, but with an `ignoreCase` option to achieve the goal (see referenced PR).

## Reference

https://github.com/Rebilly/api-definitions/pull/1590

## Testing

Test cases were added to cover the option.

## Check yourself

- [x] Code is linted
- [ ] Tested with redoc/reference-docs/workflows (internal)
- [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


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 5d205d1ca7ba8591dc00571921b100311a5af58d

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name                  | Type  |
| --------------------- | ----- |
| @redocly/openapi-core | Patch |
| @redocly/cli          | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/justnero/openapi-cli/new/ignore-case-for-tags-alphabetical?filename=.changeset/warm-bags-lie.md&value=---%0A%22%40redocly%2Fopenapi-core%22%3A%20patch%0A---%0A%0AAdd%20%60ignoreCase%60%20for%20%60tags-alphabetical%60%20rule%0A)


RomanHotsiy: @tatomyr we try to use `false` as a default value across all our configuration. So it's better to rename it to `caseSensitive` or `matchCase`.
justnero: Won't it be a breaking change, though?
RomanHotsiy: Oh, yes, I think you're right. I didn't check the PR. Our current behavior is to be case-sensitive.

I think we should keep it as is, @tatomyr.
tatomyr: 
Ok then, let's keep it. 

Patch:
diff --git a/.changeset/forty-onions-deliver.md b/.changeset/forty-onions-deliver.md
new file mode 100644
index 00000000..41b50810
--- /dev/null
+++ b/.changeset/forty-onions-deliver.md
@@ -0,0 +1,6 @@
+---
+'@redocly/openapi-core': minor
+'@redocly/cli': minor
+---
+
+Added `ignoreCase` option for `tags-alphabetical` rule.
diff --git a/docs/rules/tags-alphabetical.md b/docs/rules/tags-alphabetical.md
index 6c63606c..9b7a442a 100644
--- a/docs/rules/tags-alphabetical.md
+++ b/docs/rules/tags-alphabetical.md
@@ -33,6 +33,7 @@ This rule is intended to prevent bikeshedding and diffuse tension between teamma
 |Option|Type|Description|
 |---|---|---|
 |severity|string|Possible values: `off`, `warn`, `error`. Default `off` (in `recommended` configuration). |
+|ignoreCase|boolean|Possible values: `true`, `false`. Default `false` (in `recommended` configuration). |
 
 An example configuration:
 
diff --git a/packages/core/src/rules/common/tags-alphabetical.ts b/packages/core/src/rules/common/tags-alphabetical.ts
index 7dc22b23..c62fb061 100644
--- a/packages/core/src/rules/common/tags-alphabetical.ts
+++ b/packages/core/src/rules/common/tags-alphabetical.ts
@@ -1,14 +1,14 @@
 import { Oas3Rule, Oas2Rule } from '../../visitors';
-import { Oas2Definition } from '../../typings/swagger';
-import { Oas3Definition } from '../../typings/openapi';
+import { Oas2Definition, Oas2Tag } from '../../typings/swagger';
+import { Oas3Definition, Oas3Tag } from '../../typings/openapi';
 import { UserContext } from '../../walk';
 
-export const TagsAlphabetical: Oas3Rule | Oas2Rule = () => {
+export const TagsAlphabetical: Oas3Rule | Oas2Rule = ({ ignoreCase = false }) => {
   return {
     Root(root: Oas2Definition | Oas3Definition, { report, location }: UserContext) {
       if (!root.tags) return;
       for (let i = 0; i < root.tags.length - 1; i++) {
-        if (root.tags[i].name > root.tags[i + 1].name) {
+        if (getTagName(root.tags[i], ignoreCase) > getTagName(root.tags[i + 1], ignoreCase)) {
           report({
             message: 'The `tags` array should be in alphabetical order.',
             location: location.child(['tags', i]),
@@ -18,3 +18,7 @@ export const TagsAlphabetical: Oas3Rule | Oas2Rule = () => {
     },
   };
 };
+
+function getTagName(tag: Oas2Tag | Oas3Tag, ignoreCase: boolean): string {
+  return ignoreCase ? tag.name.toLowerCase() : tag.name;
+}


Test patch:
diff --git a/packages/core/src/rules/common/__tests__/tags-alphabetical.test.ts b/packages/core/src/rules/common/__tests__/tags-alphabetical.test.ts
index 1c22fd8e..d517d287 100644
--- a/packages/core/src/rules/common/__tests__/tags-alphabetical.test.ts
+++ b/packages/core/src/rules/common/__tests__/tags-alphabetical.test.ts
@@ -61,4 +61,62 @@ describe('Oas3 tags-alphabetical', () => {
 
     expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
   });
+
+  it('should report on tags object if not sorted alphabetically not ignoring case', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+          openapi: 3.0.0
+          paths: {}
+          tags:
+            - name: a
+            - name: B
+        `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'tags-alphabetical': 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "location": Array [
+            Object {
+              "pointer": "#/tags/0",
+              "reportOnKey": false,
+              "source": "foobar.yaml",
+            },
+          ],
+          "message": "The \`tags\` array should be in alphabetical order.",
+          "ruleId": "tags-alphabetical",
+          "severity": "error",
+          "suggest": Array [],
+        },
+      ]
+    `);
+  });
+
+  it('should not report on tags object if sorted alphabetically ignoring case', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+      openapi: 3.0.0
+      paths: {}
+      tags:
+        - name: a
+        - name: B
+        `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'tags-alphabetical': { severity: 'error', ignoreCase: true } }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
+  });
 });