Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(no-classes): add options ignoreIdentifierPattern and ignoreCodePattern #863

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
});
});
Loading