Skip to content

Commit

Permalink
feat: new rule no-class-inheritance
Browse files Browse the repository at this point in the history
fix #886
  • Loading branch information
RebeccaStevens committed Oct 20, 2024
1 parent e36178c commit 0003e5a
Show file tree
Hide file tree
Showing 10 changed files with 539 additions and 6 deletions.
1 change: 1 addition & 0 deletions .github/workflows/semantic-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
functional-parameters
immutable-data
no-classes
no-class-inheritance
no-conditional-statements
no-expression-statements
no-let
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,12 @@ The [below section](#rules) gives details on which rules are enabled by each rul

### No Other Paradigms

| Name                | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | 💭 ||
| :------------------------------------------------------- | :------------------------------------------------------------------------ | :----------------------------------- | :-- | :---------------------------- | :-- | :-- | :-- | :-- |
| [no-classes](docs/rules/no-classes.md) | Disallow classes. | ☑️ ✅ 🔒 ![badge-noOtherParadigms][] | | | | | | |
| [no-mixed-types](docs/rules/no-mixed-types.md) | Restrict types so that only members of the same kind are allowed in them. | ☑️ ✅ 🔒 ![badge-noOtherParadigms][] | | ![badge-disableTypeChecked][] | | | 💭 | |
| [no-this-expressions](docs/rules/no-this-expressions.md) | Disallow this access. | 🔒 ![badge-noOtherParadigms][] | | ☑️ ✅ | | | | |
| Name                 | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | 💭 ||
| :--------------------------------------------------------- | :------------------------------------------------------------------------ | :----------------------------------- | :-- | :---------------------------- | :-- | :-- | :-- | :-- |
| [no-class-inheritance](docs/rules/no-class-inheritance.md) | Disallow inheritance in classes. | ☑️ ✅ 🔒 ![badge-noOtherParadigms][] | | | | | | |
| [no-classes](docs/rules/no-classes.md) | Disallow classes. | ✅ 🔒 ![badge-noOtherParadigms][] | | ☑️ | | | | |
| [no-mixed-types](docs/rules/no-mixed-types.md) | Restrict types so that only members of the same kind are allowed in them. | ☑️ ✅ 🔒 ![badge-noOtherParadigms][] | | ![badge-disableTypeChecked][] | | | 💭 | |
| [no-this-expressions](docs/rules/no-this-expressions.md) | Disallow this access. | 🔒 ![badge-noOtherParadigms][] | | ☑️ ✅ | | | | |

### No Statements

Expand Down
95 changes: 95 additions & 0 deletions docs/rules/no-class-inheritance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!-- markdownlint-disable -->
<!-- begin auto-generated rule header -->

# Disallow inheritance in classes (`functional/no-class-inheritance`)

💼 This rule is enabled in the following configs: ☑️ `lite`, `noOtherParadigms`, ✅ `recommended`, 🔒 `strict`.

<!-- end auto-generated rule header -->
<!-- markdownlint-restore -->
<!-- markdownlint-restore -->

Disallow use of inheritance for classes.

## Rule Details

### ❌ Incorrect

<!-- eslint-skip -->

```js
/* eslint functional/no-class-inheritance: "error" */

abstract class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
}

class Dog extends Animal {
constructor(name, age) {
super(name, age);
}

get ageInDogYears() {
return 7 * this.age;
}
}

const dogA = new Dog("Jasper", 2);

console.log(`${dogA.name} is ${dogA.ageInDogYears} in dog years.`);
```

### ✅ Correct

```js
/* eslint functional/no-class-inheritance: "error" */

class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
}

class Dog {
constructor(name, age) {
this.animal = new Animal(name, age);
}

get ageInDogYears() {
return 7 * this.animal.age;
}
}

console.log(`${dogA.name} is ${getAgeInDogYears(dogA.age)} in dog years.`);
```

## Options

This rule accepts an options object of the following type:

```ts
type Options = {
ignoreIdentifierPattern?: string[] | string;
ignoreCodePattern?: string[] | string;
};
```

### Default Options

```ts
const defaults = {};
```

### `ignoreIdentifierPattern`

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.

### `ignoreCodePattern`

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.
2 changes: 1 addition & 1 deletion docs/rules/no-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Disallow classes (`functional/no-classes`)

💼 This rule is enabled in the following configs: ☑️ `lite`, `noOtherParadigms`, ✅ `recommended`, 🔒 `strict`.
💼🚫 This rule is enabled in the following configs: `noOtherParadigms`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the ☑️ `lite` config.

<!-- end auto-generated rule header -->
<!-- markdownlint-restore -->
Expand Down
7 changes: 7 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ const configs = await rsEslint(
"jsdoc/require-jsdoc": "off",
},
},
{
files: ["**/*.md/**"],
rules: {
"max-classes-per-file": "off",
"ts/no-extraneous-class": "off",
},
},
);

// Use our local version of the plugin.
Expand Down
2 changes: 2 additions & 0 deletions src/configs/lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint";

import * as functionalParameters from "#/rules/functional-parameters";
import * as immutableData from "#/rules/immutable-data";
import * as noClasses from "#/rules/no-classes";
import * as noConditionalStatements from "#/rules/no-conditional-statements";
import * as noExpressionStatements from "#/rules/no-expression-statements";
import * as preferImmutableTypes from "#/rules/prefer-immutable-types";
Expand All @@ -16,6 +17,7 @@ const overrides = {
},
],
[immutableData.fullName]: ["error", { ignoreClasses: "fieldsOnly" }],
[noClasses.fullName]: "off",
[noConditionalStatements.fullName]: "off",
[noExpressionStatements.fullName]: "off",
[preferImmutableTypes.fullName]: [
Expand Down
3 changes: 3 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as functionalParameters from "./functional-parameters";
import * as immutableData from "./immutable-data";
import * as noClassInheritance from "./no-class-inheritance";
import * as noClasses from "./no-classes";
import * as noConditionalStatements from "./no-conditional-statements";
import * as noExpressionStatements from "./no-expression-statements";
Expand All @@ -25,6 +26,7 @@ export const rules: Readonly<{
[functionalParameters.name]: typeof functionalParameters.rule;
[immutableData.name]: typeof immutableData.rule;
[noClasses.name]: typeof noClasses.rule;
[noClassInheritance.name]: typeof noClassInheritance.rule;
[noConditionalStatements.name]: typeof noConditionalStatements.rule;
[noExpressionStatements.name]: typeof noExpressionStatements.rule;
[noLet.name]: typeof noLet.rule;
Expand All @@ -45,6 +47,7 @@ export const rules: Readonly<{
[functionalParameters.name]: functionalParameters.rule,
[immutableData.name]: immutableData.rule,
[noClasses.name]: noClasses.rule,
[noClassInheritance.name]: noClassInheritance.rule,
[noConditionalStatements.name]: noConditionalStatements.rule,
[noExpressionStatements.name]: noExpressionStatements.rule,
[noLet.name]: noLet.rule,
Expand Down
157 changes: 157 additions & 0 deletions src/rules/no-class-inheritance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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 {
type NamedCreateRuleCustomMeta,
type Rule,
type RuleResult,
createRule,
} from "#/utils/rule";

/**
* The name of this rule.
*/
export const name = "no-class-inheritance";

/**
* The full name of this rule.
*/
export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`;

/**
* The options this rule can take.
*/
type Options = [IgnoreIdentifierPatternOption & IgnoreCodePatternOption];

/**
* The schema for the rule options.
*/
const schema: JSONSchema4[] = [
{
type: "object",
properties: deepmerge(
ignoreIdentifierPatternOptionSchema,
ignoreCodePatternOptionSchema,
),
additionalProperties: false,
},
];

/**
* The default options for the rule.
*/
const defaultOptions: Options = [{}];

/**
* The possible error messages.
*/
const errorMessages = {
abstract: "Unexpected abstract class.",
extends: "Unexpected inheritance, use composition instead.",
} as const;

/**
* The meta data for this rule.
*/
const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages> = {
type: "suggestion",
docs: {
category: "No Other Paradigms",
description: "Disallow inheritance in classes.",
recommended: "recommended",
recommendedSeverity: "error",
requiresTypeChecking: false,
},
messages: errorMessages,
schema,
};

/**
* Check if the given class node violates this rule.
*/
function checkClass(
node: ESClass,
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): RuleResult<keyof typeof errorMessages, Options> {
const [optionsObject] = options;
const { ignoreIdentifierPattern, ignoreCodePattern } = optionsObject;

const m_descriptors: Array<
RuleResult<keyof typeof errorMessages, Options>["descriptors"][number]
> = [];

if (
!shouldIgnorePattern(
node,
context,
ignoreIdentifierPattern,
undefined,
ignoreCodePattern,
)
) {
if (node.abstract) {
const nodeText = context.sourceCode.getText(node);
const abstractRelativeIndex = nodeText.indexOf("abstract");
const abstractIndex =
context.sourceCode.getIndexFromLoc(node.loc.start) +
abstractRelativeIndex;
const start = context.sourceCode.getLocFromIndex(abstractIndex);
const end = context.sourceCode.getLocFromIndex(
abstractIndex + "abstract".length,
);

m_descriptors.push({
node,
loc: {
start,
end,
},
messageId: "abstract",
});
}

if (node.superClass !== null) {
const nodeText = context.sourceCode.getText(node);
const extendsRelativeIndex = nodeText.indexOf("extends");
const extendsIndex =
context.sourceCode.getIndexFromLoc(node.loc.start) +
extendsRelativeIndex;
const start = context.sourceCode.getLocFromIndex(extendsIndex);
const { end } = node.superClass.loc;

m_descriptors.push({
node,
loc: {
start,
end,
},
messageId: "extends",
});
}
}

return {
context,
descriptors: m_descriptors,
};
}

// Create the rule.
export const rule: Rule<keyof typeof errorMessages, Options> = createRule<
keyof typeof errorMessages,
Options
>(name, meta, defaultOptions, {
ClassDeclaration: checkClass,
ClassExpression: checkClass,
});
Loading

0 comments on commit 0003e5a

Please sign in to comment.