-
Notifications
You must be signed in to change notification settings - Fork 889
new-rule: no-unnecessary-class #3119
Changes from 5 commits
d460026
5688559
44f409f
a32e011
63e9d6a
972715f
a2eb7a6
74e7866
5012ae6
bf7af78
00174ea
5a1c9e5
91bbb09
5fa280e
89ee1dc
376af9f
7047d6f
8553c22
4fe5677
5576fd0
a191ac8
176049a
cf33675
10c01a8
4de707b
09e4244
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
if (isClassDeclaration(statement) | ||
&& !hasExtendsClause(statement) | ||
&& !hasImplementsClause(statement) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class Foo {
public prop = 1;
constructor() {
class Bar {
~~~~~ [lorem ipsum ... static class]
static PROP = 2;
}
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: Personally, I would not want There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bad example-- what if There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 But you could just rename the rule to
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like the idea of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why exclude There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please rename the parameter |
||
return (statement.heritageClauses !== undefined) ? statement.heritageClauses[0].token === ts.SyntaxKind.ExtendsKeyword : false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use tsutils' |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
return isEmptyConstructor(classMembers[0]); | ||
} else { | ||
return false; | ||
} | ||
} | ||
|
||
function isEmptyConstructor(member: ts.ClassElement): boolean { | ||
if (member.kind === ts.SyntaxKind.Constructor | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
&& isFunctionWithBody(member) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||
return member.body.getFullText().trim().replace(/\s+/g, "") === "{}"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return member.body.statements.length === 0 |
||
} | ||
return false; | ||
} |
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; } | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
[0]: Classes containing only static members are disallowed. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"rules": { | ||
"no-static-only-classes": true | ||
} | ||
} |
There was a problem hiding this comment.
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.