Skip to content

Commit 9a2d3bc

Browse files
j-f1JamesHenry
authored andcommitted
[FEAT] [no-unnecessary-class] Add rule (typescript-eslint#234)
* Add `no-unnecessary-class` rule * Add metadata * ⬆️ eslint-docs
1 parent 347c543 commit 9a2d3bc

File tree

6 files changed

+298
-5
lines changed

6 files changed

+298
-5
lines changed

Diff for: packages/eslint-plugin-typescript/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@ This guarantees 100% compatibility between the plugin and the parser.
4848

4949
<!-- Please run `npm run docs` to update this section -->
5050
<!-- begin rule list -->
51+
5152
**Key**: :heavy_check_mark: = recommended, :wrench: = fixable
5253

54+
<!-- prettier-ignore -->
5355
| Name | Description | :heavy_check_mark: | :wrench: |
5456
| ---- | ----------- | ------------------ | -------- |
5557
| [`typescript/adjacent-overload-signatures`](./docs/rules/adjacent-overload-signatures.md) | Require that member overloads be consecutive (`adjacent-overload-signatures` from TSLint) | | |
@@ -68,6 +70,7 @@ This guarantees 100% compatibility between the plugin and the parser.
6870
| [`typescript/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | | :wrench: |
6971
| [`typescript/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces (`no-empty-interface` from TSLint) | | |
7072
| [`typescript/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type (`no-any` from TSLint) | | |
73+
| [`typescript/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces (`no-unnecessary-class` from TSLint) | | |
7174
| [`typescript/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean. (`no-inferrable-types` from TSLint) | | :wrench: |
7275
| [`typescript/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor`. (`no-misused-new` from TSLint) | | |
7376
| [`typescript/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces (`no-namespace` from TSLint) | | |
@@ -82,4 +85,5 @@ This guarantees 100% compatibility between the plugin and the parser.
8285
| [`typescript/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) (`interface-over-type-literal` from TSLint) | | :wrench: |
8386
| [`typescript/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules. (`no-internal-module` from TSLint) | | :wrench: |
8487
| [`typescript/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | | :wrench: |
88+
8589
<!-- end rule list -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Forbids the use of classes as namespaces (no-extraneous-class)
2+
3+
This rule warns when a class is accidentally used as a namespace.
4+
5+
## Rule Details
6+
7+
From TSLint’s docs:
8+
9+
> Users who come from a Java-style OO language may wrap their utility functions in an extra class,
10+
> instead of putting them at the top level.
11+
12+
Examples of **incorrect** code for this rule:
13+
14+
```ts
15+
class EmptyClass {}
16+
17+
class ConstructorOnly {
18+
constructor() {
19+
foo();
20+
}
21+
}
22+
23+
// Use an object instead:
24+
class StaticOnly {
25+
static version = 42;
26+
static hello() {
27+
console.log("Hello, world!");
28+
}
29+
}
30+
```
31+
32+
Examples of **correct** code for this rule:
33+
34+
```ts
35+
class EmptyClass extends SuperClass {}
36+
37+
class ParameterProperties {
38+
constructor(public name: string) {}
39+
}
40+
41+
const StaticOnly = {
42+
version: 42,
43+
hello() {
44+
console.log("Hello, world!");
45+
},
46+
};
47+
```
48+
49+
### Options
50+
51+
This rule accepts a single object option.
52+
53+
- `constructorOnly: true` will silence warnings about classes containing only a constructor.
54+
- `allowEmpty: true` will silence warnings about empty classes.
55+
- `staticOnly: true` will silence warnings about classes containing only static members.
56+
57+
## When Not To Use It
58+
59+
You can disable this rule if you don’t have anyone who would make these kinds of mistakes on your
60+
team or if you use classes as namespaces.
61+
62+
## Compatibility
63+
64+
[`no-unnecessary-class`](https://palantir.github.io/tslint/rules/no-unnecessary-class/) from TSLint
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* @fileoverview Forbids the use of classes as namespaces
3+
* @author Jed Fox
4+
*/
5+
"use strict";
6+
7+
const util = require("../util");
8+
9+
//------------------------------------------------------------------------------
10+
// Rule Definition
11+
//------------------------------------------------------------------------------
12+
13+
module.exports = {
14+
meta: {
15+
docs: {
16+
description: "Forbids the use of classes as namespaces",
17+
extraDescription: [util.tslintRule("no-unnecessary-class")],
18+
category: "Best Practices",
19+
recommended: false,
20+
url: util.metaDocsUrl("no-extraneous-class"),
21+
},
22+
fixable: null,
23+
schema: [
24+
{
25+
type: "object",
26+
additionalProperties: false,
27+
properties: {
28+
allowConstructorOnly: {
29+
type: "boolean",
30+
},
31+
allowEmpty: {
32+
type: "boolean",
33+
},
34+
allowStaticOnly: {
35+
type: "boolean",
36+
},
37+
},
38+
},
39+
],
40+
messages: {
41+
empty: "Unexpected empty class.",
42+
onlyStatic: "Unexpected class with only static properties.",
43+
onlyConstructor: "Unexpected class with only a constructor.",
44+
},
45+
},
46+
47+
create(context) {
48+
const { allowConstructorOnly, allowEmpty, allowStaticOnly } =
49+
context.options[0] || {};
50+
51+
return {
52+
ClassBody(node) {
53+
const { id, superClass } = node.parent;
54+
55+
if (superClass) return;
56+
57+
if (node.body.length === 0) {
58+
if (allowEmpty) return;
59+
context.report({ node: id, messageId: "empty" });
60+
return;
61+
}
62+
63+
let onlyStatic = true;
64+
let onlyConstructor = true;
65+
66+
for (const prop of node.body) {
67+
if (prop.kind === "constructor") {
68+
if (
69+
prop.value.params.some(
70+
param => param.type === "TSParameterProperty"
71+
)
72+
) {
73+
onlyConstructor = false;
74+
onlyStatic = false;
75+
}
76+
} else {
77+
onlyConstructor = false;
78+
if (!prop.static) {
79+
onlyStatic = false;
80+
}
81+
}
82+
if (!(onlyStatic || onlyConstructor)) break;
83+
}
84+
85+
if (onlyConstructor) {
86+
if (!allowConstructorOnly) {
87+
context.report({
88+
node: id,
89+
messageId: "onlyConstructor",
90+
});
91+
}
92+
return;
93+
}
94+
if (onlyStatic && !allowStaticOnly) {
95+
context.report({ node: id, messageId: "onlyStatic" });
96+
}
97+
},
98+
};
99+
},
100+
};

Diff for: packages/eslint-plugin-typescript/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"eslint": "^5.9.0",
2929
"eslint-config-eslint": "^5.0.1",
3030
"eslint-config-prettier": "^3.3.0",
31-
"eslint-docs": "^0.2.3",
31+
"eslint-docs": "^0.2.6",
3232
"eslint-plugin-eslint-plugin": "^2.0.0",
3333
"eslint-plugin-node": "^8.0.0",
3434
"eslint-plugin-prettier": "^3.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* @fileoverview Forbids the use of classes as namespaces
3+
* Some tests adapted from https://github.com/palantir/tslint/tree/c7fc99b5/test/rules/no-unnecessary-class
4+
* @author Jed Fox
5+
*/
6+
"use strict";
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const rule = require("../../../lib/rules/no-extraneous-class"),
13+
RuleTester = require("eslint").RuleTester;
14+
15+
const empty = { messageId: "empty", type: "Identifier" };
16+
const onlyStatic = { messageId: "onlyStatic", type: "Identifier" };
17+
const onlyConstructor = { messageId: "onlyConstructor", type: "Identifier" };
18+
19+
//------------------------------------------------------------------------------
20+
// Tests
21+
//------------------------------------------------------------------------------
22+
23+
const ruleTester = new RuleTester({
24+
parser: "typescript-eslint-parser",
25+
});
26+
27+
ruleTester.run("no-extraneous-class", rule, {
28+
valid: [
29+
`
30+
class Foo {
31+
public prop = 1;
32+
constructor() {}
33+
}
34+
`.trim(),
35+
`
36+
export class CClass extends BaseClass {
37+
public static helper(): void {}
38+
private static privateHelper(): boolean {
39+
return true;
40+
}
41+
constructor() {}
42+
}
43+
`.trim(),
44+
`
45+
class Foo {
46+
constructor(
47+
public bar: string
48+
) {}
49+
}
50+
`.trim(),
51+
{
52+
code: "class Foo {}",
53+
options: [{ allowEmpty: true }],
54+
},
55+
{
56+
code: `
57+
class Foo {
58+
constructor() {}
59+
}
60+
`.trim(),
61+
options: [{ allowConstructorOnly: true }],
62+
},
63+
{
64+
code: `
65+
export class Bar {
66+
public static helper(): void {}
67+
private static privateHelper(): boolean {
68+
return true;
69+
}
70+
}
71+
`.trim(),
72+
options: [{ allowStaticOnly: true }],
73+
},
74+
],
75+
76+
invalid: [
77+
{
78+
code: "class Foo {}",
79+
errors: [empty],
80+
},
81+
{
82+
code: `
83+
class Foo {
84+
public prop = 1;
85+
constructor() {
86+
class Bar {
87+
static PROP = 2;
88+
}
89+
}
90+
}
91+
export class Bar {
92+
public static helper(): void {}
93+
private static privateHelper(): boolean {
94+
return true;
95+
}
96+
}
97+
`.trim(),
98+
errors: [onlyStatic, onlyStatic],
99+
},
100+
{
101+
code: `
102+
class Foo {
103+
constructor() {}
104+
}
105+
`.trim(),
106+
errors: [onlyConstructor],
107+
},
108+
{
109+
code: `
110+
export class AClass {
111+
public static helper(): void {}
112+
private static privateHelper(): boolean {
113+
return true;
114+
}
115+
constructor() {
116+
class nestedClass {
117+
}
118+
}
119+
}
120+
121+
`.trim(),
122+
errors: [onlyStatic, empty],
123+
},
124+
],
125+
});

Diff for: packages/eslint-plugin-typescript/yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -707,10 +707,10 @@ eslint-config-prettier@^3.3.0:
707707
dependencies:
708708
get-stdin "^6.0.0"
709709

710-
eslint-docs@^0.2.3:
711-
version "0.2.3"
712-
resolved "https://registry.yarnpkg.com/eslint-docs/-/eslint-docs-0.2.3.tgz#71a583443c1d74bfc7c1af31b3249e258037309c"
713-
integrity sha512-6afUYuK65TOPliMul0yIGwCiKWNEqk54RbuNZhJKt1mTVHyG0D2Zbbpa7yTT1/TiCfnUkfQa7TA/l7Jgu998Dw==
710+
eslint-docs@^0.2.6:
711+
version "0.2.6"
712+
resolved "https://registry.yarnpkg.com/eslint-docs/-/eslint-docs-0.2.6.tgz#40bfbf62e22f948f02893e90280abf36ff260293"
713+
integrity sha512-XyuBu/5d51ZecfYY+cantvWDLloo5j38sHJZE3VNU0u4PJCaOYvscWXBX/9ADzXBDJF5TvBEXCqPDnK/WrOiTA==
714714
dependencies:
715715
chalk "^2.4.1"
716716
detect-newline "^2.1.0"

0 commit comments

Comments
 (0)