Repository: eslint-functional/eslint-plugin-functional (ts)

Issue:
`no-classes` ignoreCodePattern
## Suggestion

Sometimes classes can be useful in FP
For example, for [nominal typing](https://byby.dev/ts-nominal-typing)
Would be great to ignore this nominal typing classes using `ignoreCodePattern` option

Pull request:
feat(no-classes): add options ignoreIdentifierPattern and ignoreCodePattern
fix #851

Patch:
diff --git a/docs/rules/no-classes.md b/docs/rules/no-classes.md
index 66c02a3..50cceeb 100644
--- a/docs/rules/no-classes.md
+++ b/docs/rules/no-classes.md
@@ -53,43 +53,29 @@ const dogA = {
 console.log(`${dogA.name} is ${getAgeInDogYears(dogA.age)} in dog years.`);
 ```
 
-### React Examples
+## Options
 
-Thanks to libraries like [recompose](https://github.com/acdlite/recompose) and Redux's
-[React Container components](http://redux.js.org/docs/basics/UsageWithReact.html), there's not much reason to build
-Components using `React.createClass` or ES6 classes anymore. The `no-this-expressions` rule makes this explicit.
+This rule accepts an options object of the following type:
 
-```js
-const Message = React.createClass({
-  render() {
-    return <div>{this.props.message}</div>; // <- no this allowed
-  },
-});
+```ts
+type Options = {
+  ignoreIdentifierPattern?: string[] | string;
+  ignoreCodePattern?: string[] | string;
+};
 ```
 
-Instead of creating classes, you should use React 0.14's
-[Stateless Functional
-Components](https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d#.t5z2fdit6)
-and save yourself some keystrokes:
+### Default Options
 
-```js
-const Message = ({ message }) => <div>{message}</div>;
+```ts
+const defaults = {};
 ```
 
-What about lifecycle methods like `shouldComponentUpdate`?
-We can use the [recompose](https://github.com/acdlite/recompose) library to apply these optimizations to your
-Stateless Functional Components. The [recompose](https://github.com/acdlite/recompose) library relies on the fact that
-your Redux state is immutable to efficiently implement `shouldComponentUpdate` for you.
+### `ignoreIdentifierPattern`
 
-```js
-import { onlyUpdateForKeys, pure } from "recompose";
-
-const Message = ({ message }) => <div>{message}</div>;
+This option takes a RegExp string or an array of RegExp strings.
+It allows for the ability to ignore violations based on the class's name.
 
-// Optimized version of same component, using shallow comparison of props
-// Same effect as React's PureRenderMixin
-const OptimizedMessage = pure(Message);
+### `ignoreCodePattern`
 
-// Even more optimized: only updates if specific prop keys have changed
-const HyperOptimizedMessage = onlyUpdateForKeys(["message"], Message);
-```
+This option takes a RegExp string or an array of RegExp strings.
+It allows for the ability to ignore violations based on the code itself.
diff --git a/src/rules/no-classes.ts b/src/rules/no-classes.ts
index 770c884..2450f61 100644
--- a/src/rules/no-classes.ts
+++ b/src/rules/no-classes.ts
@@ -1,6 +1,14 @@
 import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
 import type { RuleContext } from "@typescript-eslint/utils/ts-eslint";
+import { deepmerge } from "deepmerge-ts";
 
+import {
+  type IgnoreCodePatternOption,
+  type IgnoreIdentifierPatternOption,
+  ignoreCodePatternOptionSchema,
+  ignoreIdentifierPatternOptionSchema,
+  shouldIgnorePattern,
+} from "#/options";
 import { ruleNameScope } from "#/utils/misc";
 import type { ESClass } from "#/utils/node-types";
 import {
@@ -23,12 +31,21 @@ export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameSco
 /**
  * The options this rule can take.
  */
-type Options = [{}];
+type Options = [IgnoreIdentifierPatternOption & IgnoreCodePatternOption];
 
 /**
  * The schema for the rule options.
  */
-const schema: JSONSchema4[] = [];
+const schema: JSONSchema4[] = [
+  {
+    type: "object",
+    properties: deepmerge(
+      ignoreIdentifierPatternOptionSchema,
+      ignoreCodePatternOptionSchema,
+    ),
+    additionalProperties: false,
+  },
+];
 
 /**
  * The default options for the rule.
@@ -64,8 +81,26 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages> = {
 function checkClass(
   node: ESClass,
   context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
+  options: Readonly<Options>,
 ): RuleResult<keyof typeof errorMessages, Options> {
-  // All class nodes violate this rule.
+  const [optionsObject] = options;
+  const { ignoreIdentifierPattern, ignoreCodePattern } = optionsObject;
+
+  if (
+    shouldIgnorePattern(
+      node,
+      context,
+      ignoreIdentifierPattern,
+      undefined,
+      ignoreCodePattern,
+    )
+  ) {
+    return {
+      context,
+      descriptors: [],
+    };
+  }
+
   return { context, descriptors: [{ node, messageId: "generic" }] };
 }
 


Test patch:
diff --git a/tests/rules/__snapshots__/no-classes.test.ts.snap b/tests/rules/__snapshots__/no-classes.test.ts.snap
index b4d296a..cc30e10 100644
--- a/tests/rules/__snapshots__/no-classes.test.ts.snap
+++ b/tests/rules/__snapshots__/no-classes.test.ts.snap
@@ -1,5 +1,37 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`no-classes > javascript - es latest > ignoreCodePattern > should report classes with non-matching identifiers 1`] = `
+[
+  {
+    "column": 1,
+    "endColumn": 13,
+    "endLine": 1,
+    "line": 1,
+    "message": "Unexpected class, use functions not classes.",
+    "messageId": "generic",
+    "nodeType": "ClassDeclaration",
+    "ruleId": "no-classes",
+    "severity": 2,
+  },
+]
+`;
+
+exports[`no-classes > javascript - es latest > options > ignoreIdentifierPattern > should report classes with non-matching identifiers 1`] = `
+[
+  {
+    "column": 1,
+    "endColumn": 13,
+    "endLine": 1,
+    "line": 1,
+    "message": "Unexpected class, use functions not classes.",
+    "messageId": "generic",
+    "nodeType": "ClassDeclaration",
+    "ruleId": "no-classes",
+    "severity": 2,
+  },
+]
+`;
+
 exports[`no-classes > javascript - es latest > reports class declarations 1`] = `
 [
   {
diff --git a/tests/rules/no-classes.test.ts b/tests/rules/no-classes.test.ts
index 05346b6..b78e895 100644
--- a/tests/rules/no-classes.test.ts
+++ b/tests/rules/no-classes.test.ts
@@ -1,3 +1,4 @@
+import dedent from "dedent";
 import { createRuleTester } from "eslint-vitest-rule-tester";
 import { describe, expect, it } from "vitest";
 
@@ -30,5 +31,51 @@ describe(name, () => {
       });
       expect(invalidResult2.messages).toMatchSnapshot();
     });
+
+    describe("options", () => {
+      describe("ignoreIdentifierPattern", () => {
+        it("should not report classes with matching identifiers", () => {
+          valid({
+            code: dedent`
+              class Foo {}
+            `,
+            options: [{ ignoreIdentifierPattern: "^Foo$" }],
+          });
+        });
+
+        it("should report classes with non-matching identifiers", () => {
+          const invalidResult = invalid({
+            code: dedent`
+              class Bar {}
+            `,
+            options: [{ ignoreIdentifierPattern: "^Foo$" }],
+            errors: ["generic"],
+          });
+          expect(invalidResult.messages).toMatchSnapshot();
+        });
+      });
+    });
+
+    describe("ignoreCodePattern", () => {
+      it("should not report classes with matching identifiers", () => {
+        valid({
+          code: dedent`
+            class Foo {}
+          `,
+          options: [{ ignoreCodePattern: "class Foo" }],
+        });
+      });
+
+      it("should report classes with non-matching identifiers", () => {
+        const invalidResult = invalid({
+          code: dedent`
+            class Bar {}
+          `,
+          options: [{ ignoreCodePattern: "class Foo" }],
+          errors: ["generic"],
+        });
+        expect(invalidResult.messages).toMatchSnapshot();
+      });
+    });
   });
 });