Skip to content

Commit

Permalink
feat(no-classes): add options ignoreIdentifierPattern and ignoreCodeP…
Browse files Browse the repository at this point in the history
…attern
  • Loading branch information
RebeccaStevens committed Aug 3, 2024
1 parent 4358c08 commit a356f8c
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 33 deletions.
46 changes: 16 additions & 30 deletions docs/rules/no-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
41 changes: 38 additions & 3 deletions src/rules/no-classes.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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" }] };
}

Expand Down
32 changes: 32 additions & 0 deletions tests/rules/__snapshots__/no-classes.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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`] = `
[
{
Expand Down
47 changes: 47 additions & 0 deletions tests/rules/no-classes.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dedent from "dedent";
import { createRuleTester } from "eslint-vitest-rule-tester";
import { describe, expect, it } from "vitest";

Expand Down Expand Up @@ -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();
});
});
});
});

0 comments on commit a356f8c

Please sign in to comment.