Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

new-rule: no-unnecessary-class #3119

Merged
merged 26 commits into from
Oct 20, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d460026
ignoring classes w heritage clauses; test for all static/no constructor
Aug 11, 2017
5688559
files
Aug 11, 2017
44f409f
ignoring empty class in lines.ts for now
Aug 11, 2017
a32e011
unsafe anys removed
Aug 11, 2017
63e9d6a
removed disable tag in lines.ts
Aug 14, 2017
972715f
addresses all requests except export default class {static FOO = 1}
Aug 15, 2017
a2eb7a6
Merge branch 'master' into no-static-only-classes
Aug 17, 2017
74e7866
isEmptyClass returns true for constructor-only classes
Aug 17, 2017
5012ae6
using .every rather than allMembersAreConstructors function
Aug 17, 2017
bf7af78
added support for nested class declarations within constructors
Aug 18, 2017
00174ea
test file updates
Aug 19, 2017
5a1c9e5
no-unnecessary-class w/ config proposal
Aug 19, 2017
91bbb09
member functions -> functions...
Aug 19, 2017
5fa280e
assigning result of node.members.every(...) instead of calling multip…
Aug 19, 2017
89ee1dc
Merge branch 'master' of https://github.com/palantir/tslint into no-s…
Sep 29, 2017
376af9f
Change requests addressed; additional logic to address contructor-onl…
Sep 29, 2017
7047d6f
using various Array methods over for of loops for readability
Sep 30, 2017
8553c22
'static int var' is not a thing in typescript...
Sep 30, 2017
4fe5677
test updates
Sep 30, 2017
5576fd0
moved walker logic into separate function
Sep 30, 2017
a191ac8
now throws failure in nested classes as well
Sep 30, 2017
176049a
removed some nested ifs; simplified checkMember logic
Sep 30, 2017
cf33675
removed unnec for of block; parsing options before they make it to wa…
Oct 1, 2017
10c01a8
Merge branch 'master' into no-static-only-classes
Oct 3, 2017
4de707b
Description update; pulled in latest master
Oct 3, 2017
09e4244
Merge branch 'master' into no-static-only-classes
ajafff Oct 6, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const rules = {
"no-string-literal": true,
"no-string-throw": true,
"no-sparse-arrays": true,
"no-static-only-classes": true,
"no-submodule-imports": true,
"no-unbound-method": true,
"no-unsafe-any": true,
Expand Down
106 changes: 106 additions & 0 deletions src/rules/noStaticOnlyClassesRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @license
* Copyright 2017 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as ts from "typescript";
import * as Lint from "../index";

import { isClassDeclaration, isFunctionWithBody } from "tsutils";

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "no-static-only-classes",
description: Lint.Utils.dedent`
Disallows classes containing only static members. Classes
with non-empty constructors are ignored.`,
rationale: Lint.Utils.dedent`
Users who come from a Java-style OO language may wrap
their utility functions in an extra class, instead of
putting them at the top level.`,
optionsDescription: "Not configurable.",
options: null,
optionExamples: [true],
type: "functionality",
typescriptOnly: false,
};

public static FAILURE_STRING = "Classes containing only static members are disallowed.";

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new NoStaticOnlyClassesWalker(sourceFile, this.ruleName, this.ruleArguments));
}
}

class NoStaticOnlyClassesWalker extends Lint.AbstractWalker<string[]> {
public walk(sourceFile: ts.SourceFile) {
for (const statement of sourceFile.statements) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that will only check classes at the top level of the file. The rule should also check classes nested in functions, namespaces, ...
You can look at class-name for an example on how to iterate the AST correctly.

if (isClassDeclaration(statement)
&& !hasExtendsClause(statement)
&& !hasImplementsClause(statement)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can ignore implemented interfaces, because the class will have instance properties or methods if the interface is not empty.

&& !isEmptyClass(statement)) {
for (const member of statement.members) {
if (!hasStaticModifier(member.modifiers) && !isEmptyConstructor(member)) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return will cause nested classes to not be checked. Although this might not occur often in real world code, I think it makes sense to also find this error:

class Foo {
    public prop = 1;
    constructor() {
        class Bar {
        ~~~~~ [lorem ipsum ... static class]
            static PROP = 2;
        }
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on a fix but could use your input:

Currently this code is passing in the test file:
class Foo { constructor() { class Bar { private a: SomeType; static PROP = 2; } } }

Personally, I would not want class Foo in my codebase, but should it throw an error?

Copy link
Contributor Author

@aervin aervin Aug 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad example-- what if private a was public a?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say as soon as there is an instance property or method, the rule should not complain. Therefore class Bar in your example is allowed.
class Foo on the other hand is pretty useless without any property or method. But it's out of scope for this rule (at least with the current name).

But you could just rename the rule to no-unnecessary-class (or similar) and check for

  • only static members (as it currently does) -> could be converted to a namespace or plain object
  • constructor only classes -> could be converted to a function
  • a mix of the above
  • completely empty classes? - maybe add an option to ignore them

Copy link
Contributor Author

@aervin aervin Aug 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the idea of no-unnecessary-class, but user-config would need some discussion? We could start with a sensible default setting for the rule. I don't see much point in a class with static members and constructors only... Is there a real-life coding scenario where this is useful? I'd be interested to learn.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see much point in a class with static members and constructors only... Is there a real-life coding scenario where this is useful?

It can be useful for writing compilers, where classes simulate discriminated unions (much like "case classes" in Scala). For example:

class While extends Statement {
    constructor(public condition: Expression, public body: Statement[]) { super(); }
}
class Break extends Statement {}
class Continue extends Statement {}

Then you'd check if a statement is a break using stmt instanceof Break.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well that makes a lot of sense @lfairy. Have you looked over the test cases for this rule? What do you make of this rule's scope?

}
}
if (statement.name !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why exclude export default class {static FOO = 1}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know this is a possibility. What should the failure message look like in this scenario? I've been underlining the class name to indicate a problem with the class itself.

this.addFailure(statement.name.pos + 1, statement.name.end, Rule.FAILURE_STRING);
}
}
}
}
}

function hasExtendsClause(statement: ts.ClassDeclaration): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please rename the parameter statement to something like declaration. Same for the function below.

return (statement.heritageClauses !== undefined) ? statement.heritageClauses[0].token === ts.SyntaxKind.ExtendsKeyword : false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for a ternary expression, simply use &&

}

function hasImplementsClause(statement: ts.ClassDeclaration): boolean {
return (statement.heritageClauses !== undefined) ? statement.heritageClauses[0].token === ts.SyntaxKind.ImplementsKeyword : false;
}

function hasStaticModifier(modifiers: ts.NodeArray<ts.Modifier> | undefined): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use tsutils' hasModifier(node.modifiers, ts.SyntaxKind.StaticKeyword) instead of this function

if (modifiers === undefined) {
return false;
}
for (const modifier of modifiers) {
if (modifier.kind === ts.SyntaxKind.StaticKeyword) {
return true;
}
}
return false;
}

function isEmptyClass(statement: ts.ClassDeclaration): boolean {
const classMembers = statement.members;
if (classMembers.length === 0) {
return true;
} else if (classMembers.length === 1 && classMembers[0].kind === ts.SyntaxKind.Constructor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to check if the class has an empty constructor?
you can simplify this whole function body to return statement.members === 0; without changing the semantics of the rule.

return isEmptyConstructor(classMembers[0]);
} else {
return false;
}
}

function isEmptyConstructor(member: ts.ClassElement): boolean {
if (member.kind === ts.SyntaxKind.Constructor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use isConstructorDeclaration(member) instead

&& isFunctionWithBody(member)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is not needed because you already check if it has a body on the next line

&& member.body !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add && !member.parameters.some(isParameterProperty) to exit if the constructor has parameter properties

return member.body.getFullText().trim().replace(/\s+/g, "") === "{}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return member.body.statements.length === 0

}
return false;
}
69 changes: 69 additions & 0 deletions test/rules/no-static-only-classes/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
class EmptyClass {}

class EmptyClass { constructor() {} }

export class PascalClass {
~~~~~~~~~~~ [0]
public static helper(): void {}
private static privateHelper(): boolean {
return true;
}
constructor() {}
}

export class PascalClass {
~~~~~~~~~~~ [0]
public static helper(): void {}
private static privateHelper(): boolean {
return true;
}
}

export class ClassA implements IInterfacey {
public static helper(): void {}
private static privateHelper(): boolean {
return true;
}
constructor() {}
}

export class ClassB extends BaseClass {
public static helper(): void {}
private static privateHelper(): boolean {
return true;
}
constructor() {}
}

export class PascalClass {
public static helper(): void {}
private static privateHelper(): boolean {
return true;
}
constructor() { this.that = what; }
}

export class ShorthandPropsClass {
public static helper(): void {}
constructor(private a: string) {}
}

export class PascalClass {
public static helper(): void {}
private static privateHelper(): boolean {
return true;
}
public index = 0;
constructor() {}
}

export class PascalClass {
public static helper(): void {}
private Helper(): boolean {
return true;
}
constructor() { this.that = what; }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like a test for shorthand properties as well, e.g.:

export class PascalClass {
    public static helper(): void {}
    constructor(private that: string) {}
}

This should not raise a warning because the private that implicitly declares a non-static property.


[0]: Classes containing only static members are disallowed.

5 changes: 5 additions & 0 deletions test/rules/no-static-only-classes/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-static-only-classes": true
}
}