-
Notifications
You must be signed in to change notification settings - Fork 889
Conversation
src/verify/lines.ts
Outdated
export class Line { } | ||
/* tslint:enable:no-static-only-classes */ |
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.
I would prefer that an empty class is not counted as a static-only class -- I think it would be confusing if it did.
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.
You're right I'll add a check for that.
return true; | ||
} | ||
constructor() { this.that = what; } | ||
} |
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.
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.
src/rules/noStaticOnlyClassesRule.ts
Outdated
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 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.
src/rules/noStaticOnlyClassesRule.ts
Outdated
|
||
class NoStaticOnlyClassesWalker extends Lint.AbstractWalker<string[]> { | ||
public walk(sourceFile: ts.SourceFile) { | ||
for (const statement of sourceFile.statements) { |
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.
src/rules/noStaticOnlyClassesRule.ts
Outdated
return; | ||
} | ||
} | ||
if (statement.name !== undefined) { |
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.
why exclude export default class {static FOO = 1}
?
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.
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.
src/rules/noStaticOnlyClassesRule.ts
Outdated
} | ||
|
||
function hasExtendsClause(statement: ts.ClassDeclaration): boolean { | ||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
no need for a ternary expression, simply use &&
src/rules/noStaticOnlyClassesRule.ts
Outdated
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 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
src/rules/noStaticOnlyClassesRule.ts
Outdated
|
||
function isEmptyConstructor(member: ts.ClassElement): boolean { | ||
if (member.kind === ts.SyntaxKind.Constructor | ||
&& isFunctionWithBody(member) |
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.
this line is not needed because you already check if it has a body on the next line
src/rules/noStaticOnlyClassesRule.ts
Outdated
function isEmptyConstructor(member: ts.ClassElement): boolean { | ||
if (member.kind === ts.SyntaxKind.Constructor | ||
&& isFunctionWithBody(member) | ||
&& member.body !== undefined) { |
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.
Add && !member.parameters.some(isParameterProperty)
to exit if the constructor has parameter properties
src/rules/noStaticOnlyClassesRule.ts
Outdated
} | ||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
use isConstructorDeclaration(member)
instead
src/rules/noStaticOnlyClassesRule.ts
Outdated
if (member.kind === ts.SyntaxKind.Constructor | ||
&& isFunctionWithBody(member) | ||
&& member.body !== undefined) { | ||
return member.body.getFullText().trim().replace(/\s+/g, "") === "{}"; |
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.
return member.body.statements.length === 0
Thanks for all the feedback! I'll get started later today. |
src/rules/noStaticOnlyClassesRule.ts
Outdated
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 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.
src/rules/noStaticOnlyClassesRule.ts
Outdated
} | ||
} | ||
|
||
function hasExtendsClause(statement: ts.ClassDeclaration): boolean { |
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.
Can you please rename the parameter statement
to something like declaration
. Same for the function below.
src/rules/noStaticOnlyClassesRule.ts
Outdated
return; | ||
} | ||
} | ||
if (node.name !== undefined) { |
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.
If you show the error on the class
keyword instead of the class' name, you don't need this condition.
this.addFailureAtNode(getChildOfKind(node, ts.SyntaxKind.ClassKeyword, this.sourceFile)!, Rule.FAILURE_STRING);
src/rules/noStaticOnlyClassesRule.ts
Outdated
* An "empty class" for our purposes. | ||
*/ | ||
function isEmptyClass(declaration: ts.ClassDeclaration): boolean { | ||
return declaration.members.length === 0 || declaration.members.every(isConstructorDeclaration); |
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.
declaration.members.every(isConstructorDeclaration)
because possibility of overloaded constructor? Would like to know if this check is necessary...
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 check makes sense to correctly handle constructor overloads.
On a side note, you could remove the length check here because Array.prototype.every
returns true
for empty arrays.
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.
Your reviews always make me go "oh right... duh!" lol Taking care of this now. However, I only see one suggestion rather than two?
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.
You're, I forgot to save the last comment before submitting 😒
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.
Code looks good so far. Added two suggestions
src/rules/noStaticOnlyClassesRule.ts
Outdated
* An "empty class" for our purposes. | ||
*/ | ||
function isEmptyClass(declaration: ts.ClassDeclaration): boolean { | ||
return declaration.members.length === 0 || declaration.members.every(isConstructorDeclaration); |
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 check makes sense to correctly handle constructor overloads.
On a side note, you could remove the length check here because Array.prototype.every
returns true
for empty arrays.
src/rules/noStaticOnlyClassesRule.ts
Outdated
for (const member of node.members) { | ||
if (isConstructorWithShorthandProps(member) || | ||
(!isConstructorDeclaration(member) && !hasModifier(member.modifiers, ts.SyntaxKind.StaticKeyword))) { | ||
return; |
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.
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;
}
}
}
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.
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?
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.
bad example-- what if private a
was public a
?
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.
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
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.
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.
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.
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
.
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.
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?
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.
Sorry for the delay. I left some comments.
Note to myself: it may make sense to also disallow classes without state, i.e. no instance properties, if it does not implement an interface or extend another class.
That can be implemented afterwards.
src/rules/noUnnecessaryClassRule.ts
Outdated
} | ||
|
||
private hasOption(option: string): boolean { | ||
return this.options.indexOf(option) !== -1; |
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.
Consider parsing the options upfront and passing an object to the constructor of your Walker.
src/rules/noUnnecessaryClassRule.ts
Outdated
if (node.members.length === 0 && !this.hasOption(OPTION__ALLOW_EMPTY_CLASS)) { | ||
this.addFailureAtNode(getChildOfKind(node, ts.SyntaxKind.ClassKeyword)!, Rule.FAILURE_EMPTY_CLASS); | ||
return; | ||
} else if (node.members.length == 0) { |
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.
use a nested if statement above instead of duplicating parts of the condition in else-if
src/rules/noUnnecessaryClassRule.ts
Outdated
public walk(sourceFile: ts.SourceFile) { | ||
const checkIfStaticOnlyClass = (node: ts.Node): void => { | ||
if (isClassDeclaration(node) && !hasExtendsClause(node)) { | ||
if (node.members.length === 0 && !this.hasOption(OPTION__ALLOW_EMPTY_CLASS)) { |
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.
Consider moving the remainder of this function to a separate method for readability.
src/rules/noUnnecessaryClassRule.ts
Outdated
return; | ||
} | ||
|
||
if (node.members.some(isConstructorWithClassDeclaration)) { |
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.
I'd prefer not to have this special case ignored. If there is only a constructor that contains a class declaration, the class is still constructor only.
src/rules/noUnnecessaryClassRule.ts
Outdated
|
||
const allMembersAreConstructors = node.members.every(isConstructorDeclaration); | ||
|
||
if (allMembersAreConstructors && !this.hasOption(OPTION__ALLOW_CONSTRUCTOR_ONLY)) { |
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.
You need to check if the constructor has parameter properties, either by default or add an option for that.
Thanks @ajafff! I'll try to get to these changes later today. |
…tatic-only-classes
Hey @ajafff, sorry for the delay. |
src/rules/noUnnecessaryClassRule.ts
Outdated
public walk(sourceFile: ts.SourceFile) { | ||
const checkIfUnnecessaryClass = (node: ts.Node): void => { | ||
if (isClassDeclaration(node) && !hasExtendsClause(node)) { | ||
return this.checkMembers(node, checkIfUnnecessaryClass); |
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.
don't return here
src/rules/noUnnecessaryClassRule.ts
Outdated
} | ||
|
||
/* Check for classes nested in constructors */ | ||
for (const member of node.members) { |
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.
you can remove this block if you remove the return at the call site (see my other comment above)
src/rules/noUnnecessaryClassRule.ts
Outdated
public static FAILURE_EMPTY_CLASS = "This class has no members."; | ||
|
||
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { | ||
return this.applyWithWalker(new NoUnnecessaryClassWalker(sourceFile, this.ruleName, this.ruleArguments)); |
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.
we typically parse the options before passing them to the Walker.
I mean something like this:
new NoUnnecessaryClassWalker(sourceFile, this.ruleName, {
allowConstructorOnly: this.ruleArguments.indexOf(OPTION__ALLOW_CONSTRUCTOR_ONLY) !== -1,
allowEmptyClass: this.ruleArguments.indexOf(OPTION__ALLOW_EMPTY_CLASS) !== -1,
allowStaticOnly: this.ruleArguments.indexOf(OPTION__ALLOW_STATIC_ONLY) !== -1,
})
constructor() { | ||
class Bar { | ||
public static helper(): void {} | ||
private private Helper(): boolean { |
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.
double private
@ajafff I think the description/rationale still needs to be updated. Do you have ideas? |
The rationale is still correct. |
thanks @aervin! |
PR checklist
Overview of change:
Adds the "no-static-only-classes" rule as outlined in the suggestion post.
This rule ignores a class which:
Is there anything you'd like reviewers to focus on?