Skip to content

Commit a4f7443

Browse files
committed
Add new rule no-for-statement as requested in #54.
1 parent 3c49b3d commit a4f7443

File tree

6 files changed

+170
-41
lines changed

6 files changed

+170
-41
lines changed

README.md

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![code style: prettier][prettier-image]][prettier-url]
77
[![MIT license][license-image]][license-url]
88

9-
[TSLint](https://palantir.github.io/tslint/) rules to disable mutation in TypeScript.
9+
[TSLint](https://palantir.github.io/tslint/) rules to disable mutation in TypeScript.
1010

1111
## Background
1212

@@ -47,6 +47,7 @@ In addition to immutable rules this project also contains a few rules for enforc
4747
* [no-mixed-interface](#no-mixed-interface)
4848
* [no-expression-statement](#no-expression-statement)
4949
* [no-if-statement](#no-if-statement)
50+
* [no-for-statement](#no-for-statement)
5051
* [Recommended built-in rules](#recommended-built-in-rules)
5152

5253
## Immutability rules
@@ -60,23 +61,32 @@ Below is some information about the `readonly` modifier and the benefits of usin
6061
You might think that using `const` would eliminate mutation from your TypeScript code. **Wrong.** Turns out that there's a pretty big loophole in `const`.
6162

6263
```typescript
63-
interface Point { x: number, y: number }
64+
interface Point {
65+
x: number;
66+
y: number;
67+
}
6468
const point: Point = { x: 23, y: 44 };
6569
point.x = 99; // This is legal
6670
```
6771

6872
This is why the `readonly` modifier exists. It prevents you from assigning a value to the result of a member expression.
6973

7074
```typescript
71-
interface Point { readonly x: number, readonly y: number }
75+
interface Point {
76+
readonly x: number;
77+
readonly y: number;
78+
}
7279
const point: Point = { x: 23, y: 44 };
7380
point.x = 99; // <- No object mutation allowed.
7481
```
7582

76-
This is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However the `readonly` modifier has **no run-time cost**, and is enforced at **compile time**. A good alternative to object mutation is to use the ES2016 object spread [syntax](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#object-spread-and-rest) that was added in typescript 2.1:
83+
This is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However the `readonly` modifier has **no run-time cost**, and is enforced at **compile time**. A good alternative to object mutation is to use the ES2016 object spread [syntax](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#object-spread-and-rest) that was added in typescript 2.1:
7784

7885
```typescript
79-
interface Point { readonly x: number, readonly y: number }
86+
interface Point {
87+
readonly x: number;
88+
readonly y: number;
89+
}
8090
const point: Point = { x: 23, y: 44 };
8191
const transformedPoint = { ...point, x: 99 };
8292
```
@@ -90,26 +100,31 @@ let { [action.id]: deletedItem, ...rest } = state;
90100
The `readonly` modifier also works on indexers:
91101

92102
```typescript
93-
const foo: { readonly [key: string]: number } = { "a": 1, "b": 2 };
103+
const foo: { readonly [key: string]: number } = { a: 1, b: 2 };
94104
foo["a"] = 3; // Error: Index signature only permits reading
95105
```
96106

97107
#### Has Fixer
108+
98109
Yes
99110

100111
#### Options
101-
- [ignore-local](#using-the-ignore-local-option)
102-
- [ignore-class](#using-the-ignore-class-option)
103-
- [ignore-interface](#using-the-ignore-interface-option)
104-
- [ignore-prefix](#using-the-ignore-prefix-option)
112+
113+
* [ignore-local](#using-the-ignore-local-option)
114+
* [ignore-class](#using-the-ignore-class-option)
115+
* [ignore-interface](#using-the-ignore-interface-option)
116+
* [ignore-prefix](#using-the-ignore-prefix-option)
105117

106118
#### Example config
119+
107120
```javascript
108121
"readonly-keyword": true
109122
```
123+
110124
```javascript
111125
"readonly-keyword": [true, "ignore-local"]
112126
```
127+
113128
```javascript
114129
"readonly-keyword": [true, "ignore-local", {"ignore-prefix": "mutable"}]
115130
```
@@ -123,38 +138,50 @@ Below is some information about the `ReadonlyArray<T>` type and the benefits of
123138
Even if an array is declared with `const` it is still possible to mutate the contents of the array.
124139

125140
```typescript
126-
interface Point { readonly x: number, readonly y: number }
141+
interface Point {
142+
readonly x: number;
143+
readonly y: number;
144+
}
127145
const points: Array<Point> = [{ x: 23, y: 44 }];
128146
points.push({ x: 1, y: 2 }); // This is legal
129147
```
130148

131149
Using the `ReadonlyArray<T>` type will stop this mutation:
132150

133151
```typescript
134-
interface Point { readonly x: number, readonly y: number }
152+
interface Point {
153+
readonly x: number;
154+
readonly y: number;
155+
}
135156
const points: ReadonlyArray<Point> = [{ x: 23, y: 44 }];
136157
points.push({ x: 1, y: 2 }); // Unresolved method push()
137158
```
138159

139160
#### Has Fixer
161+
140162
Yes
141163

142164
#### Options
143-
- [ignore-local](#using-the-ignore-local-option)
144-
- [ignore-prefix](#using-the-ignore-prefix-option)
165+
166+
* [ignore-local](#using-the-ignore-local-option)
167+
* [ignore-prefix](#using-the-ignore-prefix-option)
145168

146169
#### Example config
170+
147171
```javascript
148172
"readonly-array": true
149173
```
174+
150175
```javascript
151176
"readonly-array": [true, "ignore-local"]
152177
```
178+
153179
```javascript
154180
"readonly-array": [true, "ignore-local", {"ignore-prefix": "mutable"}]
155181
```
156182

157183
### no-let
184+
158185
This rule should be combined with tslint's built-in `no-var-keyword` rule to enforce that all variables are declared as `const`.
159186

160187
There's no reason to use `let` in a Redux/React application, because all your state is managed by either Redux or React. Use `const` instead, and avoid state bugs altogether.
@@ -166,69 +193,82 @@ let x = 5; // <- Unexpected let or var, use const.
166193
What about `for` loops? Loops can be replaced with the Array methods like `map`, `filter`, and so on. If you find the built-in JS Array methods lacking, use [ramda](http://ramdajs.com/), or [lodash-fp](https://github.com/lodash/lodash/wiki/FP-Guide).
167194

168195
```typescript
169-
const SearchResults =
170-
({ results }) =>
171-
<ul>{
172-
results.map(result => <li>result</li>) // <- Who needs let?
173-
}</ul>;
196+
const SearchResults = ({ results }) => (
197+
<ul>
198+
{results.map(result => <li>result</li>) // <- Who needs let?
199+
}
200+
</ul>
201+
);
174202
```
175203

176204
#### Has Fixer
205+
177206
Yes
178207

179208
#### Options
180-
- [ignore-local](#using-the-ignore-local-option)
181-
- [ignore-prefix](#using-the-ignore-prefix-option)
209+
210+
* [ignore-local](#using-the-ignore-local-option)
211+
* [ignore-prefix](#using-the-ignore-prefix-option)
182212

183213
#### Example config
214+
184215
```javascript
185216
"no-let": true
186217
```
218+
187219
```javascript
188220
"no-let": [true, "ignore-local"]
189221
```
222+
190223
```javascript
191224
"no-let": [true, "ignore-local", {"ignore-prefix": "mutable"}]
192225
```
193226

194227
### no-object-mutation
228+
195229
This rule prohibits syntax that mutates existing objects via assignment to or deletion of their properties. While requiring the `readonly` modifier forces declared types to be immutable, it won't stop assignment into or modification of untyped objects or external types declared under different rules. Forbidding forms like `a.b = 'c'` is one way to plug this hole. Inspired by the no-mutation rule of [eslint-plugin-immutable](https://github.com/jhusain/eslint-plugin-immutable).
196230

197231
```typescript
198-
const x = {a: 1};
232+
const x = { a: 1 };
199233

200-
x.foo = 'bar'; // <- Modifying properties of existing object not allowed.
234+
x.foo = "bar"; // <- Modifying properties of existing object not allowed.
201235
x.a += 1; // <- Modifying properties of existing object not allowed.
202236
delete x.a; // <- Modifying properties of existing object not allowed.
203237
```
204238

205239
#### Has Fixer
240+
206241
No
207242

208243
#### Options
209-
- [ignore-prefix](#using-the-ignore-prefix-option)
244+
245+
* [ignore-prefix](#using-the-ignore-prefix-option)
210246

211247
#### Example config
248+
212249
```javascript
213250
"no-object-mutation": true
214251
```
252+
215253
```javascript
216254
"no-object-mutation": [true, {"ignore-prefix": "mutable"}]
217255
```
218256

219257
### no-method-signature
258+
220259
There are two ways function members can be declared in an interface or type alias:
221260

222261
```typescript
223262
interface Zoo {
224-
foo(): string, // MethodSignature, cannot have readonly modifier
225-
readonly bar: () => string, // PropertySignature
263+
foo(): string; // MethodSignature, cannot have readonly modifier
264+
readonly bar: () => string; // PropertySignature
226265
}
227266
```
228267

229268
The `MethodSignature` and the `PropertySignature` forms seem equivalent, but only the `PropertySignature` form can have a `readonly` modifier. Becuase of this any `MethodSignature` will be mutable. Therefore the `no-method-signature` rule disallows usage of this form and instead proposes to use the `PropertySignature` which can have a `readonly` modifier. It should be noted however that the `PropertySignature` form for declaring functions does not support overloading.
230269

231270
### no-delete
271+
232272
The delete operator allows for mutating objects by deleting keys. This rule disallows any delete expressions.
233273

234274
```typescript
@@ -238,58 +278,62 @@ delete object.property; // Unexpected delete, objects should be considered immut
238278
As an alternative the spread operator can be used to delete a key in an object (as noted [here](https://stackoverflow.com/a/35676025/2761797)):
239279

240280
```typescript
241-
const { [action.id]: deletedItem, ...rest } = state
281+
const { [action.id]: deletedItem, ...rest } = state;
242282
```
243283

244284
## Functional style rules
245285

246286
### no-this, no-class
287+
247288
Thanks to libraries like [recompose](https://github.com/acdlite/recompose) and Redux's [React Container components](http://redux.js.org/docs/basics/UsageWithReact.html), there's not much reason to build Components using `React.createClass` or ES6 classes anymore. The `no-this` rule makes this explicit.
248289

249290
```typescript
250291
const Message = React.createClass({
251292
render: function() {
252-
return <div>{ this.props.message }</div>; // <- no this allowed
293+
return <div>{this.props.message}</div>; // <- no this allowed
253294
}
254-
})
295+
});
255296
```
297+
256298
Instead of creating classes, you should use React 0.14's [Stateless Functional Components](https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d#.t5z2fdit6) and save yourself some keystrokes:
257299

258300
```typescript
259-
const Message = ({message}) => <div>{ message }</div>;
301+
const Message = ({ message }) => <div>{message}</div>;
260302
```
261303

262304
What about lifecycle methods like `shouldComponentUpdate`? We can use the [recompose](https://github.com/acdlite/recompose) library to apply these optimizations to your Stateless Functional Components. The [recompose](https://github.com/acdlite/recompose) library relies on the fact that your Redux state is immutable to efficiently implement shouldComponentUpdate for you.
263305

264306
```typescript
265-
import { pure, onlyUpdateForKeys } from 'recompose';
307+
import { pure, onlyUpdateForKeys } from "recompose";
266308

267-
const Message = ({message}) => <div>{ message }</div>;
309+
const Message = ({ message }) => <div>{message}</div>;
268310

269311
// Optimized version of same component, using shallow comparison of props
270312
// Same effect as React's PureRenderMixin
271313
const OptimizedMessage = pure(Message);
272314

273315
// Even more optimized: only updates if specific prop keys have changed
274-
const HyperOptimizedMessage = onlyUpdateForKeys(['message'], Message);
316+
const HyperOptimizedMessage = onlyUpdateForKeys(["message"], Message);
275317
```
276318

277319
### no-mixed-interface
278320

279321
Mixing functions and data properties in the same interface is a sign of object-orientation style. This rule enforces that an inteface only has one type of members, eg. only data properties or only functions.
280322

281323
### no-expression-statement
324+
282325
When you call a function and don’t use it’s return value, chances are high that it is being called for its side effect. e.g.
283326

284327
```typescript
285-
array.push(1)
286-
alert('Hello world!')
328+
array.push(1);
329+
alert("Hello world!");
287330
```
288331

289332
This rule checks that the value of an expression is assigned to a variable and thus helps promote side-effect free (pure) functions.
290333

291334
#### Options
292-
- [ignore-prefix](#using-the-ignore-prefix-option-with-no-expression-statement)
335+
336+
* [ignore-prefix](#using-the-ignore-prefix-option-with-no-expression-statement)
293337

294338
#### Example config
295339

@@ -306,11 +350,12 @@ This rule checks that the value of an expression is assigned to a variable and t
306350
```
307351

308352
### no-if-statement
353+
309354
If statements is not a good fit for functional style programming as they are not expresssions and do not return a value. This rule disallows if statements.
310355

311356
```typescript
312357
let x;
313-
if(i === 1) {
358+
if (i === 1) {
314359
x = 2;
315360
} else {
316361
x = 3;
@@ -325,6 +370,27 @@ const x = i === 1 ? 2 : 3;
325370

326371
For more background see this [blog post](https://hackernoon.com/rethinking-javascript-the-if-statement-b158a61cd6cb) and discussion in [#54](https://github.com/jonaskello/tslint-immutable/issues/54).
327372

373+
### no-for-statement
374+
375+
In functional programming we want everthing to be an expression that returns a value. The `for` statement is not an expression. This rule disallows for statements, including `for of` and `for in`.
376+
377+
```typescript
378+
const numbers = [1, 2, 3];
379+
const double = [];
380+
for (let i = 0; i < numbers.length; i++) {
381+
double[i] = numbers[i] * 2;
382+
}
383+
```
384+
385+
Instead consider using `map` or `reduce`:
386+
387+
```typescript
388+
const numbers = [1, 2, 3];
389+
const double = numbers.map(n => n * 2);
390+
```
391+
392+
For more background see this [blog post](https://hackernoon.com/rethinking-javascript-death-of-the-for-loop-c431564c84a8) and discussion in [#54](https://github.com/jonaskello/tslint-immutable/issues/54).
393+
328394
## Options
329395

330396
### Using the `ignore-local` option
@@ -337,9 +403,11 @@ The quote above is from the [clojure docs](https://clojure.org/reference/transie
337403
Note that using this option can lead to more imperative code in functions so use with care!
338404

339405
### Using the `ignore-class` option
406+
340407
Doesn't check for `readonly` in classes.
341408

342409
### Using the `ignore-interface` option
410+
343411
Doesn't check for `readonly` in interfaces.
344412

345413
### Using the `ignore-prefix` option
@@ -357,8 +425,8 @@ Typescript is not immutable by default but it can be if you use this package. So
357425

358426
```typescript
359427
type person = {
360-
readonly name: string,
361-
mutableAge: number // This is OK with ignore-prefix = "mutable"
428+
readonly name: string;
429+
mutableAge: number; // This is OK with ignore-prefix = "mutable"
362430
};
363431
```
364432

src/noClassRule.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ function checkNode(
1818
node: ts.Node,
1919
_ctx: Lint.WalkContext<Options>
2020
): CheckNodeResult {
21-
return (node && node.kind === ts.SyntaxKind.ClassKeyword) ||
22-
node.kind === ts.SyntaxKind.ClassDeclaration
21+
return node &&
22+
(node.kind === ts.SyntaxKind.ClassKeyword ||
23+
node.kind === ts.SyntaxKind.ClassDeclaration)
2324
? { invalidNodes: [createInvalidNode(node)] }
2425
: {
2526
invalidNodes: []

0 commit comments

Comments
 (0)