Skip to content

Commit 68b4275

Browse files
feat: new rule no-class-inheritance
fix #886
1 parent 2a6b099 commit 68b4275

File tree

10 files changed

+534
-6
lines changed

10 files changed

+534
-6
lines changed

.github/workflows/semantic-pr.yml

Lines changed: 1 addition & 0 deletions
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

Lines changed: 6 additions & 5 deletions
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

Lines changed: 95 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

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

0 commit comments

Comments
 (0)