-
Notifications
You must be signed in to change notification settings - Fork 885
Added prefer-readonly rule with fixes #2896
Changes from 13 commits
267ad63
c88cdb2
f7abbe0
e37e2e3
0c1fb66
2da094e
efea6ec
569ac5a
f5fdb8e
42a336d
ae96afb
a8ea477
69543a4
b68d4cf
4dd52ce
b0c2b11
1f2e78d
946b52e
a20e2c8
fbdda51
8efa5bc
6918b76
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,120 @@ | ||
/** | ||
* @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 utils from "tsutils"; | ||
import * as ts from "typescript"; | ||
|
||
const OUTSIDE_CONSTRUCTOR = -1; | ||
const DIRECTLY_INSIDE_CONSTRUCTOR = 0; | ||
|
||
export type ParameterOrPropertyDeclaration = ts.ParameterDeclaration | ts.PropertyDeclaration; | ||
|
||
function typeIsOrHasBaseType(type: ts.Type, parentType: ts.Type) { | ||
if (type.symbol === undefined || parentType.symbol === undefined) { | ||
return false; | ||
} | ||
|
||
for (const baseType of [type, ...type.getBaseTypes()]) { | ||
if (baseType.symbol !== undefined && baseType.symbol.name === parentType.symbol.name) { | ||
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. out of curiosity, is comparing by name really the best thing we can do here? 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. Agreed. I didn't find a better alternative - would welcome suggestions! 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. That's probably ok for now. |
||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
export class ClassScope { | ||
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 know there's one bit of I know I mentioned in another thread that it's cool to have multiple files for a rule implementation, but in this case I think we can reasonably avoid it. |
||
private readonly privateModifiableMembers = new Map<string, ParameterOrPropertyDeclaration>(); | ||
private readonly privateModifiableStatics = new Map<string, ParameterOrPropertyDeclaration>(); | ||
private readonly memberVariableModifications = new Set<string>(); | ||
private readonly staticVariableModifications = new Set<string>(); | ||
|
||
private readonly typeChecker: ts.TypeChecker; | ||
private readonly classType: ts.Type; | ||
|
||
private constructorScopeDepth = OUTSIDE_CONSTRUCTOR; | ||
|
||
public constructor(classNode: ts.Node, typeChecker: ts.TypeChecker) { | ||
this.classType = typeChecker.getTypeAtLocation(classNode); | ||
this.typeChecker = typeChecker; | ||
} | ||
|
||
public addDeclaredVariable(node: ParameterOrPropertyDeclaration) { | ||
if (!utils.isModfierFlagSet(node, ts.ModifierFlags.Private) | ||
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'll release a new version 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. Is there a reason to still use the misnamed |
||
|| utils.isModfierFlagSet(node, ts.ModifierFlags.Readonly) | ||
|| node.name.kind === ts.SyntaxKind.ComputedPropertyName) { | ||
return; | ||
} | ||
|
||
if (utils.isModfierFlagSet(node, ts.ModifierFlags.Static)) { | ||
this.privateModifiableStatics.set(node.name.getText(), node); | ||
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. the rule should probably just ignore properties with computed names also the current implementation would consider these to be equal: private foo;
private 'foo';
private "foo"; If you ignore computed property names, you can simply use |
||
} else { | ||
this.privateModifiableMembers.set(node.name.getText(), node); | ||
} | ||
} | ||
|
||
public addVariableModification(node: ts.PropertyAccessExpression) { | ||
const modifierType = this.typeChecker.getTypeAtLocation(node.expression); | ||
if (modifierType.symbol === undefined || !typeIsOrHasBaseType(modifierType, this.classType)) { | ||
return; | ||
} | ||
|
||
const toStatic = utils.isObjectType(modifierType) && utils.isObjectFlagSet(modifierType, ts.ObjectFlags.Anonymous); | ||
if (!toStatic && this.constructorScopeDepth === DIRECTLY_INSIDE_CONSTRUCTOR) { | ||
return; | ||
} | ||
|
||
const variable = node.name.text; | ||
|
||
(toStatic ? this.staticVariableModifications : this.memberVariableModifications).add(variable); | ||
} | ||
|
||
public enterConstructor() { | ||
this.constructorScopeDepth = DIRECTLY_INSIDE_CONSTRUCTOR; | ||
} | ||
|
||
public exitConstructor() { | ||
this.constructorScopeDepth = OUTSIDE_CONSTRUCTOR; | ||
} | ||
|
||
public enterNonConstructorScope() { | ||
if (this.constructorScopeDepth !== OUTSIDE_CONSTRUCTOR) { | ||
this.constructorScopeDepth += 1; | ||
} | ||
} | ||
|
||
public exitNonConstructorScope() { | ||
if (this.constructorScopeDepth !== OUTSIDE_CONSTRUCTOR) { | ||
this.constructorScopeDepth -= 1; | ||
} | ||
} | ||
|
||
public finalizeUnmodifiedPrivateNonReadonlys() { | ||
this.memberVariableModifications.forEach((variableName) => { | ||
this.privateModifiableMembers.delete(variableName); | ||
}); | ||
|
||
this.staticVariableModifications.forEach((variableName) => { | ||
this.privateModifiableStatics.delete(variableName); | ||
}); | ||
|
||
return [ | ||
...Array.from(this.privateModifiableMembers.values()), | ||
...Array.from(this.privateModifiableStatics.values()), | ||
]; | ||
} | ||
} |
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 probably belongs in a more generic utils file? maybe
src/language/typeUtils.ts