Skip to content

Commit 53df2fd

Browse files
feat: new rule no-class-inheritance (#890)
fix #886
1 parent 579170c commit 53df2fd

File tree

10 files changed

+512
-6
lines changed

10 files changed

+512
-6
lines changed

.github/workflows/semantic-pr.yml

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
functional-parameters
3333
immutable-data
3434
no-classes
35+
no-class-inheritance
3536
no-conditional-statements
3637
no-expression-statements
3738
no-let

README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,12 @@ The [below section](#rules) gives details on which rules are enabled by each rul
135135

136136
### No Other Paradigms
137137

138-
| Name                | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | 💭 ||
139-
| :------------------------------------------------------- | :------------------------------------------------------------------------ | :----------------------------------- | :-- | :---------------------------- | :-- | :-- | :-- | :-- |
140-
| [no-classes](docs/rules/no-classes.md) | Disallow classes. | ☑️ ✅ 🔒 ![badge-noOtherParadigms][] | | | | | | |
141-
| [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][] | | | 💭 | |
142-
| [no-this-expressions](docs/rules/no-this-expressions.md) | Disallow this access. | 🔒 ![badge-noOtherParadigms][] | | ☑️ ✅ | | | | |
138+
| Name                 | Description | 💼 | ⚠️ | 🚫 | 🔧 | 💡 | 💭 ||
139+
| :--------------------------------------------------------- | :------------------------------------------------------------------------ | :----------------------------------- | :-- | :---------------------------- | :-- | :-- | :-- | :-- |
140+
| [no-class-inheritance](docs/rules/no-class-inheritance.md) | Disallow inheritance in classes. | ☑️ ✅ 🔒 ![badge-noOtherParadigms][] | | | | | | |
141+
| [no-classes](docs/rules/no-classes.md) | Disallow classes. | ✅ 🔒 ![badge-noOtherParadigms][] | | ☑️ | | | | |
142+
| [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][] | | | 💭 | |
143+
| [no-this-expressions](docs/rules/no-this-expressions.md) | Disallow this access. | 🔒 ![badge-noOtherParadigms][] | | ☑️ ✅ | | | | |
143144

144145
### No Statements
145146

docs/rules/no-class-inheritance.md

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<!-- markdownlint-disable -->
2+
<!-- begin auto-generated rule header -->
3+
4+
# Disallow inheritance in classes (`functional/no-class-inheritance`)
5+
6+
💼 This rule is enabled in the following configs: ☑️ `lite`, `noOtherParadigms`, ✅ `recommended`, 🔒 `strict`.
7+
8+
<!-- end auto-generated rule header -->
9+
<!-- markdownlint-restore -->
10+
<!-- markdownlint-restore -->
11+
12+
Disallow use of inheritance for classes.
13+
14+
## Rule Details
15+
16+
### ❌ Incorrect
17+
18+
<!-- eslint-skip -->
19+
20+
```js
21+
/* eslint functional/no-class-inheritance: "error" */
22+
23+
abstract class Animal {
24+
constructor(name, age) {
25+
this.name = name;
26+
this.age = age;
27+
}
28+
}
29+
30+
class Dog extends Animal {
31+
constructor(name, age) {
32+
super(name, age);
33+
}
34+
35+
get ageInDogYears() {
36+
return 7 * this.age;
37+
}
38+
}
39+
40+
const dogA = new Dog("Jasper", 2);
41+
42+
console.log(`${dogA.name} is ${dogA.ageInDogYears} in dog years.`);
43+
```
44+
45+
### ✅ Correct
46+
47+
```js
48+
/* eslint functional/no-class-inheritance: "error" */
49+
50+
class Animal {
51+
constructor(name, age) {
52+
this.name = name;
53+
this.age = age;
54+
}
55+
}
56+
57+
class Dog {
58+
constructor(name, age) {
59+
this.animal = new Animal(name, age);
60+
}
61+
62+
get ageInDogYears() {
63+
return 7 * this.animal.age;
64+
}
65+
}
66+
67+
console.log(`${dogA.name} is ${getAgeInDogYears(dogA.age)} in dog years.`);
68+
```
69+
70+
## Options
71+
72+
This rule accepts an options object of the following type:
73+
74+
```ts
75+
type Options = {
76+
ignoreIdentifierPattern?: string[] | string;
77+
ignoreCodePattern?: string[] | string;
78+
};
79+
```
80+
81+
### Default Options
82+
83+
```ts
84+
const defaults = {};
85+
```
86+
87+
### `ignoreIdentifierPattern`
88+
89+
This option takes a RegExp string or an array of RegExp strings.
90+
It allows for the ability to ignore violations based on the class's name.
91+
92+
### `ignoreCodePattern`
93+
94+
This option takes a RegExp string or an array of RegExp strings.
95+
It allows for the ability to ignore violations based on the code itself.

docs/rules/no-classes.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

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

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

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

eslint.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ const configs = await rsEslint(
130130
embeddedLanguageFormatting: "off",
131131
},
132132
],
133+
"max-classes-per-file": "off",
134+
"ts/no-extraneous-class": "off",
133135
},
134136
},
135137
);

src/configs/lite.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint";
22

33
import * as functionalParameters from "#/rules/functional-parameters";
44
import * as immutableData from "#/rules/immutable-data";
5+
import * as noClasses from "#/rules/no-classes";
56
import * as noConditionalStatements from "#/rules/no-conditional-statements";
67
import * as noExpressionStatements from "#/rules/no-expression-statements";
78
import * as preferImmutableTypes from "#/rules/prefer-immutable-types";
@@ -16,6 +17,7 @@ const overrides = {
1617
},
1718
],
1819
[immutableData.fullName]: ["error", { ignoreClasses: "fieldsOnly" }],
20+
[noClasses.fullName]: "off",
1921
[noConditionalStatements.fullName]: "off",
2022
[noExpressionStatements.fullName]: "off",
2123
[preferImmutableTypes.fullName]: [

src/rules/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as functionalParameters from "./functional-parameters";
22
import * as immutableData from "./immutable-data";
3+
import * as noClassInheritance from "./no-class-inheritance";
34
import * as noClasses from "./no-classes";
45
import * as noConditionalStatements from "./no-conditional-statements";
56
import * as noExpressionStatements from "./no-expression-statements";
@@ -25,6 +26,7 @@ export const rules: Readonly<{
2526
[functionalParameters.name]: typeof functionalParameters.rule;
2627
[immutableData.name]: typeof immutableData.rule;
2728
[noClasses.name]: typeof noClasses.rule;
29+
[noClassInheritance.name]: typeof noClassInheritance.rule;
2830
[noConditionalStatements.name]: typeof noConditionalStatements.rule;
2931
[noExpressionStatements.name]: typeof noExpressionStatements.rule;
3032
[noLet.name]: typeof noLet.rule;
@@ -45,6 +47,7 @@ export const rules: Readonly<{
4547
[functionalParameters.name]: functionalParameters.rule,
4648
[immutableData.name]: immutableData.rule,
4749
[noClasses.name]: noClasses.rule,
50+
[noClassInheritance.name]: noClassInheritance.rule,
4851
[noConditionalStatements.name]: noConditionalStatements.rule,
4952
[noExpressionStatements.name]: noExpressionStatements.rule,
5053
[noLet.name]: noLet.rule,

src/rules/no-class-inheritance.ts

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
2+
import type { RuleContext } from "@typescript-eslint/utils/ts-eslint";
3+
import { deepmerge } from "deepmerge-ts";
4+
5+
import {
6+
type IgnoreCodePatternOption,
7+
type IgnoreIdentifierPatternOption,
8+
ignoreCodePatternOptionSchema,
9+
ignoreIdentifierPatternOptionSchema,
10+
shouldIgnorePattern,
11+
} from "#/options";
12+
import { ruleNameScope } from "#/utils/misc";
13+
import type { ESClass } from "#/utils/node-types";
14+
import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule";
15+
16+
/**
17+
* The name of this rule.
18+
*/
19+
export const name = "no-class-inheritance";
20+
21+
/**
22+
* The full name of this rule.
23+
*/
24+
export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`;
25+
26+
/**
27+
* The options this rule can take.
28+
*/
29+
type Options = [IgnoreIdentifierPatternOption & IgnoreCodePatternOption];
30+
31+
/**
32+
* The schema for the rule options.
33+
*/
34+
const schema: JSONSchema4[] = [
35+
{
36+
type: "object",
37+
properties: deepmerge(ignoreIdentifierPatternOptionSchema, ignoreCodePatternOptionSchema),
38+
additionalProperties: false,
39+
},
40+
];
41+
42+
/**
43+
* The default options for the rule.
44+
*/
45+
const defaultOptions: Options = [{}];
46+
47+
/**
48+
* The possible error messages.
49+
*/
50+
const errorMessages = {
51+
abstract: "Unexpected abstract class.",
52+
extends: "Unexpected inheritance, use composition instead.",
53+
} as const;
54+
55+
/**
56+
* The meta data for this rule.
57+
*/
58+
const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages> = {
59+
type: "suggestion",
60+
docs: {
61+
category: "No Other Paradigms",
62+
description: "Disallow inheritance in classes.",
63+
recommended: "recommended",
64+
recommendedSeverity: "error",
65+
requiresTypeChecking: false,
66+
},
67+
messages: errorMessages,
68+
schema,
69+
};
70+
71+
/**
72+
* Check if the given class node violates this rule.
73+
*/
74+
function checkClass(
75+
node: ESClass,
76+
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
77+
options: Readonly<Options>,
78+
): RuleResult<keyof typeof errorMessages, Options> {
79+
const [optionsObject] = options;
80+
const { ignoreIdentifierPattern, ignoreCodePattern } = optionsObject;
81+
82+
const mut_descriptors: Array<RuleResult<keyof typeof errorMessages, Options>["descriptors"][number]> = [];
83+
84+
if (!shouldIgnorePattern(node, context, ignoreIdentifierPattern, undefined, ignoreCodePattern)) {
85+
if (node.abstract) {
86+
const nodeText = context.sourceCode.getText(node);
87+
const abstractRelativeIndex = nodeText.indexOf("abstract");
88+
const abstractIndex = context.sourceCode.getIndexFromLoc(node.loc.start) + abstractRelativeIndex;
89+
const start = context.sourceCode.getLocFromIndex(abstractIndex);
90+
const end = context.sourceCode.getLocFromIndex(abstractIndex + "abstract".length);
91+
92+
mut_descriptors.push({
93+
node,
94+
loc: {
95+
start,
96+
end,
97+
},
98+
messageId: "abstract",
99+
});
100+
}
101+
102+
if (node.superClass !== null) {
103+
const nodeText = context.sourceCode.getText(node);
104+
const extendsRelativeIndex = nodeText.indexOf("extends");
105+
const extendsIndex = context.sourceCode.getIndexFromLoc(node.loc.start) + extendsRelativeIndex;
106+
const start = context.sourceCode.getLocFromIndex(extendsIndex);
107+
const { end } = node.superClass.loc;
108+
109+
mut_descriptors.push({
110+
node,
111+
loc: {
112+
start,
113+
end,
114+
},
115+
messageId: "extends",
116+
});
117+
}
118+
}
119+
120+
return {
121+
context,
122+
descriptors: mut_descriptors,
123+
};
124+
}
125+
126+
// Create the rule.
127+
export const rule: Rule<keyof typeof errorMessages, Options> = createRule<keyof typeof errorMessages, Options>(
128+
name,
129+
meta,
130+
defaultOptions,
131+
{
132+
ClassDeclaration: checkClass,
133+
ClassExpression: checkClass,
134+
},
135+
);

0 commit comments

Comments
 (0)