Skip to content

Commit 51fc4f2

Browse files
committed
Add prefer const rule
1 parent b8e5a89 commit 51fc4f2

3 files changed

Lines changed: 238 additions & 1 deletion

File tree

Jakefile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,7 @@ var tslintRuleDir = "scripts/tslint";
859859
var tslintRules = ([
860860
"nextLineRule",
861861
"noNullRule",
862+
"preferConstRule",
862863
"booleanTriviaRule",
863864
"typeOperatorSpacingRule"
864865
]);

scripts/tslint/preferConstRule.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/// <reference path="../../node_modules/tslint/typings/typescriptServices.d.ts" />
2+
/// <reference path="../../node_modules/tslint/lib/tslint.d.ts" />
3+
4+
5+
export class Rule extends Lint.Rules.AbstractRule {
6+
public static FAILURE_STRING_FACTORY = (identifier: string) => `Identifier '${identifier}' never appears on the LHS of an assignment - use const instead of let for its declaration.`;
7+
8+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
9+
return this.applyWithWalker(new PreferConstWalker(sourceFile, this.getOptions()));
10+
}
11+
}
12+
13+
function isBindingPattern(node: ts.Node): node is ts.BindingPattern {
14+
return !!node && (node.kind === ts.SyntaxKind.ArrayBindingPattern || node.kind === ts.SyntaxKind.ObjectBindingPattern);
15+
}
16+
17+
function walkUpBindingElementsAndPatterns(node: ts.Node): ts.Node {
18+
while (node && (node.kind === ts.SyntaxKind.BindingElement || isBindingPattern(node))) {
19+
node = node.parent;
20+
}
21+
22+
return node;
23+
}
24+
25+
function getCombinedNodeFlags(node: ts.Node): ts.NodeFlags {
26+
node = walkUpBindingElementsAndPatterns(node);
27+
28+
let flags = node.flags;
29+
if (node.kind === ts.SyntaxKind.VariableDeclaration) {
30+
node = node.parent;
31+
}
32+
33+
if (node && node.kind === ts.SyntaxKind.VariableDeclarationList) {
34+
flags |= node.flags;
35+
node = node.parent;
36+
}
37+
38+
if (node && node.kind === ts.SyntaxKind.VariableStatement) {
39+
flags |= node.flags;
40+
}
41+
42+
return flags;
43+
}
44+
45+
function isLet(node: ts.Node) {
46+
return !!(getCombinedNodeFlags(node) & ts.NodeFlags.Let);
47+
}
48+
49+
function isExported(node: ts.Node) {
50+
return !!(getCombinedNodeFlags(node) & ts.NodeFlags.Export);
51+
}
52+
53+
function isAssignmentOperator(token: ts.SyntaxKind): boolean {
54+
return token >= ts.SyntaxKind.FirstAssignment && token <= ts.SyntaxKind.LastAssignment;
55+
}
56+
57+
function isBindingLiteralExpression(node: ts.Node): node is (ts.ArrayLiteralExpression | ts.ObjectLiteralExpression) {
58+
return (!!node) && (node.kind === ts.SyntaxKind.ObjectLiteralExpression || node.kind === ts.SyntaxKind.ArrayLiteralExpression);
59+
}
60+
61+
interface DeclarationUsages {
62+
declaration: ts.VariableDeclaration;
63+
usages: number;
64+
}
65+
66+
class PreferConstWalker extends Lint.RuleWalker {
67+
private inScopeLetDeclarations: ts.Map<DeclarationUsages>[] = [];
68+
private errors: Lint.RuleFailure[] = [];
69+
private markAssignment(identifier: ts.Identifier) {
70+
const name = identifier.text;
71+
for (var i = this.inScopeLetDeclarations.length - 1; i >= 0; i--) {
72+
var declarations = this.inScopeLetDeclarations[i];
73+
if (declarations[name]) {
74+
declarations[name].usages++;
75+
break;
76+
}
77+
}
78+
}
79+
80+
visitSourceFile(node: ts.SourceFile) {
81+
super.visitSourceFile(node);
82+
// Sort errors by position because tslint doesn't
83+
this.errors.sort((a, b) => a.getStartPosition().getPosition() - b.getStartPosition().getPosition()).forEach(e => this.addFailure(e));
84+
}
85+
86+
visitBinaryExpression(node: ts.BinaryExpression) {
87+
if (isAssignmentOperator(node.operatorToken.kind)) {
88+
this.visitLHSExpressions(node.left);
89+
}
90+
super.visitBinaryExpression(node);
91+
}
92+
93+
private visitLHSExpressions(node: ts.Expression) {
94+
while (node.kind === ts.SyntaxKind.ParenthesizedExpression) {
95+
node = (node as ts.ParenthesizedExpression).expression;
96+
}
97+
if (node.kind === ts.SyntaxKind.Identifier) {
98+
this.markAssignment(node as ts.Identifier);
99+
}
100+
else if (isBindingLiteralExpression(node)) {
101+
this.visitBindingLiteralExpression(node as (ts.ArrayLiteralExpression | ts.ObjectLiteralExpression));
102+
}
103+
}
104+
105+
private visitBindingLiteralExpression(node: ts.ArrayLiteralExpression | ts.ObjectLiteralExpression) {
106+
if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) {
107+
const pattern = node as ts.ObjectLiteralExpression;
108+
for (let i = 0; i < pattern.properties.length; i++) {
109+
const element = pattern.properties[i];
110+
if (element.name.kind === ts.SyntaxKind.Identifier) {
111+
this.markAssignment(element.name as ts.Identifier)
112+
}
113+
else if (isBindingPattern(element.name)) {
114+
this.visitBindingPatternIdentifiers(element.name as ts.BindingPattern);
115+
}
116+
}
117+
}
118+
else if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) {
119+
const pattern = node as ts.ArrayLiteralExpression;
120+
for (let i = 0; i < pattern.elements.length; i++) {
121+
const element = pattern.elements[i];
122+
this.visitLHSExpressions(element);
123+
}
124+
}
125+
}
126+
127+
private visitBindingPatternIdentifiers(pattern: ts.BindingPattern) {
128+
for (let i = 0; i < pattern.elements.length; i++) {
129+
const element = pattern.elements[i];
130+
if (element.name.kind === ts.SyntaxKind.Identifier) {
131+
this.markAssignment(element.name as ts.Identifier);
132+
}
133+
else {
134+
this.visitBindingPatternIdentifiers(element.name as ts.BindingPattern);
135+
}
136+
}
137+
}
138+
139+
visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression) {
140+
this.visitAnyUnaryExpression(node);
141+
super.visitPrefixUnaryExpression(node);
142+
}
143+
144+
visitPostfixUnaryExpression(node: ts.PostfixUnaryExpression) {
145+
this.visitAnyUnaryExpression(node);
146+
super.visitPostfixUnaryExpression(node);
147+
}
148+
149+
private visitAnyUnaryExpression(node: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression) {
150+
if (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) {
151+
this.visitLHSExpressions(node.operand);
152+
}
153+
}
154+
155+
visitModuleDeclaration(node: ts.ModuleDeclaration) {
156+
if (node.body.kind === ts.SyntaxKind.ModuleBlock) {
157+
// For some reason module blocks are left out of the visit block traversal
158+
this.visitBlock(node.body as ts.ModuleBlock);
159+
}
160+
super.visitModuleDeclaration(node);
161+
}
162+
163+
visitForOfStatement(node: ts.ForOfStatement) {
164+
this.visitAnyForStatement(node);
165+
super.visitForOfStatement(node);
166+
this.popDeclarations();
167+
}
168+
169+
visitForInStatement(node: ts.ForInStatement) {
170+
this.visitAnyForStatement(node);
171+
super.visitForInStatement(node);
172+
this.popDeclarations();
173+
}
174+
175+
private visitAnyForStatement(node: ts.ForOfStatement | ts.ForInStatement) {
176+
let names: ts.Map<DeclarationUsages> = {};
177+
if (isLet(node.initializer)) {
178+
if (node.initializer.kind === ts.SyntaxKind.VariableDeclarationList) {
179+
this.collectLetIdentifiers(node.initializer as ts.VariableDeclarationList, names);
180+
}
181+
}
182+
this.inScopeLetDeclarations.push(names);
183+
}
184+
185+
private popDeclarations() {
186+
const completed = this.inScopeLetDeclarations.pop();
187+
for (const name in completed) {
188+
if (Object.hasOwnProperty.call(completed, name)) {
189+
const element = completed[name];
190+
if (element.usages === 0) {
191+
this.errors.push(this.createFailure(element.declaration.getStart(this.getSourceFile()), element.declaration.getWidth(this.getSourceFile()), Rule.FAILURE_STRING_FACTORY(name)));
192+
}
193+
}
194+
}
195+
}
196+
197+
visitBlock(node: ts.Block) {
198+
let names: ts.Map<DeclarationUsages> = {};
199+
for (let i = 0; i < node.statements.length; i++) {
200+
const statement = node.statements[i];
201+
if (statement.kind === ts.SyntaxKind.VariableStatement) {
202+
this.collectLetIdentifiers((statement as ts.VariableStatement).declarationList, names);
203+
}
204+
}
205+
this.inScopeLetDeclarations.push(names);
206+
super.visitBlock(node);
207+
this.popDeclarations();
208+
}
209+
210+
private collectLetIdentifiers(list: ts.VariableDeclarationList, ret: ts.Map<DeclarationUsages>) {
211+
const children = list.declarations;
212+
for (let i = 0; i < children.length; i++) {
213+
const node = children[i];
214+
if (isLet(node) && !isExported(node)) {
215+
this.collectNameIdentifiers(node, node.name, ret);
216+
}
217+
}
218+
}
219+
220+
private collectNameIdentifiers(value: ts.VariableDeclaration, node: ts.Identifier | ts.BindingPattern, table: ts.Map<DeclarationUsages>) {
221+
if (node.kind === ts.SyntaxKind.Identifier) {
222+
table[(node as ts.Identifier).text] = {declaration: value, usages: 0};
223+
}
224+
else {
225+
this.collectBindingPatternIdentifiers(value, node as ts.BindingPattern, table);
226+
}
227+
}
228+
229+
private collectBindingPatternIdentifiers(value: ts.VariableDeclaration, pattern: ts.BindingPattern, table: ts.Map<DeclarationUsages>) {
230+
for (let i = 0; i < pattern.elements.length; i++) {
231+
const element = pattern.elements[i];
232+
this.collectNameIdentifiers(value, element.name, table);
233+
}
234+
}
235+
}

tslint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"no-inferrable-types": true,
4040
"no-null": true,
4141
"boolean-trivia": true,
42-
"type-operator-spacing": true
42+
"type-operator-spacing": true,
43+
"prefer-const": true
4344
}
4445
}

0 commit comments

Comments
 (0)