Skip to content
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
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
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ const configs = await rsEslint(
embeddedLanguageFormatting: "off",
},
],
"max-classes-per-file": "off",
"ts/no-extraneous-class": "off",
},
},
);
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
135 changes: 135 additions & 0 deletions src/rules/no-class-inheritance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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 mut_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);

mut_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;

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

return {
context,
descriptors: mut_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
Loading