tslint-immutable
Advanced tools
Comparing version 3.0.0 to 3.1.0
@@ -8,7 +8,9 @@ # Change Log | ||
## [Unreleased] | ||
### Added | ||
## [v3.1.0] - 2017-04-05 | ||
### Added | ||
- `ignore-local` option added to `readonly-array`. | ||
- `ignore-prefix` option added to `readonly-array`. | ||
### Changed | ||
## [v3.0.0] - 2017-04-02 | ||
@@ -19,3 +21,3 @@ ### Changed | ||
### Added | ||
- readonly-array now also checks for implicit arrays. | ||
- `readonly-array` now also checks for implicity declared mutable arrays. | ||
@@ -56,3 +58,5 @@ ## [v2.1.1] - 2017-03-29 | ||
[Unreleased]: https://github.com/jonaskello/tslint-immutable/compare/v2.1.2...master | ||
[Unreleased]: https://github.com/jonaskello/tslint-immutable/compare/v3.1.0...master | ||
[v3.0.0]: https://github.com/jonaskello/tslint-immutable/compare/v3.0.0...v3.1.0 | ||
[v3.0.0]: https://github.com/jonaskello/tslint-immutable/compare/v2.1.1...v3.0.0 | ||
[v2.1.2]: https://github.com/jonaskello/tslint-immutable/compare/v2.1.1...v2.1.2 | ||
@@ -59,0 +63,0 @@ [v2.1.1]: https://github.com/jonaskello/tslint-immutable/compare/v2.1.0...v2.1.1 |
{ | ||
"name": "tslint-immutable", | ||
"version": "3.0.0", | ||
"version": "3.1.0", | ||
"description": "TSLint rules to disable mutation in TypeScript.", | ||
@@ -28,2 +28,3 @@ "main": "index.js", | ||
"build": "rm -rf rules && tsc -p tsconfig.json", | ||
"lint": "tslint './src/**/*.ts{,x}'", | ||
"test": "yarn build && yarn test:rules", | ||
@@ -30,0 +31,0 @@ "test:rules": "./scripts/test-rules.sh", |
135
README.md
@@ -33,2 +33,19 @@ # tslint-immutable | ||
* [Immutability rules](#immutability-rules) | ||
* [readonly-interface](#readonly-interface) | ||
* [readonly-indexer](#readonly-indexer) | ||
* [readonly-array](#readonly-array) | ||
* [no-let](#no-let) | ||
* [Functional style rules](#functional-style-rules) | ||
* [no-this](#no-this-no-class-no-new) | ||
* [no-class](#no-this-no-class-no-new) | ||
* [no-new](#no-this-no-class-no-new) | ||
* [no-mixed-interface](#no-mixed-interface) | ||
* [no-expression-statement](#no-expression-statement) | ||
* [Other rules](#other-rules) | ||
* [no-arguments](#no-arguments) | ||
* [no-label](#no-label) | ||
* [no-semicolon-interface](#no-semicolon-interface) | ||
* [import-containment](#import-containment) | ||
### Immutability rules | ||
@@ -42,3 +59,3 @@ | ||
```TypeScript | ||
```typescript | ||
interface Point { x: number, y: number } | ||
@@ -51,3 +68,3 @@ const point: Point = { x: 23, y: 44 }; | ||
```TypeScript | ||
```typescript | ||
interface Point { readonly x: number, readonly y: number } | ||
@@ -60,3 +77,3 @@ const point: Point = { x: 23, y: 44 }; | ||
```TypeScript | ||
```typescript | ||
interface Point { readonly x: number, readonly y: number } | ||
@@ -71,3 +88,3 @@ const point: Point = { x: 23, y: 44 }; | ||
```TypeScript | ||
```typescript | ||
// NOT OK | ||
@@ -85,3 +102,3 @@ let foo: { [key:string]: number }; | ||
```TypeScript | ||
```typescript | ||
interface Point { readonly x: number, readonly y: number } | ||
@@ -94,3 +111,3 @@ const points: Array<Point> = [{ x: 23, y: 44 }]; | ||
```TypeScript | ||
```typescript | ||
interface Point { readonly x: number, readonly y: number } | ||
@@ -101,8 +118,23 @@ const points: ReadonlyArray<Point> = [{ x: 23, y: 44 }]; | ||
#### no-let | ||
This rule should be combined with tslint's built-in `no-var` rule to enforce that all variables are declared as `const`. | ||
Options: | ||
- [ignore-local](#using-the-ignore-local-option) | ||
- [ignore-prefix](#using-the-ignore-prefix-option) | ||
Example config: | ||
```javascript | ||
"readonly-array": true | ||
``` | ||
```javascript | ||
"readonly-array": [true, "ignore-local"] | ||
``` | ||
```javascript | ||
"readonly-array": [true, "ignore-local", {"ignore-prefix": "mutable"}] | ||
``` | ||
#### no-let | ||
This rule should be combined with tslint's built-in `no-var-keyword` rule to enforce that all variables are declared as `const`. | ||
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. | ||
```TypeScript | ||
```typescript | ||
let x = 5; // <- Unexpected let or var, use const. | ||
@@ -113,3 +145,3 @@ ``` | ||
```TypeScript | ||
```typescript | ||
const SearchResults = | ||
@@ -122,6 +154,8 @@ ({ results }) => | ||
### Functional style rules | ||
#### no-this, no-class, no-new | ||
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. | ||
```TypeScript | ||
```typescript | ||
const Message = React.createClass({ | ||
@@ -135,3 +169,3 @@ render: function() { | ||
```TypeScript | ||
```typescript | ||
const Message = ({message}) => <div>{ message }</div>; | ||
@@ -142,3 +176,3 @@ ``` | ||
```TypeScript | ||
```typescript | ||
import { pure, onlyUpdateForKeys } from 'recompose'; | ||
@@ -156,4 +190,2 @@ | ||
### Functional style rules | ||
#### no-mixed-interface | ||
@@ -166,3 +198,3 @@ | ||
```TypeScript | ||
```typescript | ||
array.push(1) | ||
@@ -176,6 +208,2 @@ alert('Hello world!') | ||
#### import-containment | ||
ECMAScript modules does not have a concept of a library that can span multiple files and share internal members. If you have a set of files that forms an library, and they need to be able to call each other internally without exposing members to other files outside the library set, this rule can be useful. | ||
#### no-arguments | ||
@@ -193,3 +221,3 @@ | ||
```TypeScript | ||
```typescript | ||
// This is NOT ok. | ||
@@ -207,14 +235,71 @@ inferface Foo { | ||
#### import-containment | ||
ECMAScript modules does not have a concept of a library that can span multiple files and share internal members. If you have a set of files that forms an library, and they need to be able to call each other internally without exposing members to other files outside the library set, this rule can be useful. | ||
## Options | ||
### Using the `ignore-local` option | ||
> If a tree falls in the woods, does it make a sound? | ||
> If a pure function mutates some local data in order to produce an immutable return value, is that ok? | ||
The quote above is from the [clojure docs](https://clojure.org/reference/transients). In general, it is more important to enforce immutability for state that is passed in and out of functions than for local state used for internal calculations within a function. For example in Redux, the state going in and out of reducers needs to be immutable while the reducer may be allowed to mutate local state in its calculations in order to achieve higher performance. This is what the `ignore-local` option enables. With this option enabled immutability will be enforced everywhere but in local state. | ||
### Using the `ignore-prefix` option | ||
Some languages are immutable by default but allows you to explicitly declare mutable variables. For example in [reason](https://facebook.github.io/reason/) you can declare mutable record fields like this: | ||
```reason | ||
type person = { | ||
name: string, | ||
mutable age: int | ||
}; | ||
``` | ||
Typescript is not immutable by default but it can be if you use this package. So in order to create an escape hatch similar to how it is done in reason the `ignore-mutable` option can be used. For example if you configure it to ignore variables with names that has the prefix "mutable" you can emulate the above example in typescript like this: | ||
```typescript | ||
type person = { | ||
readonly name: string, | ||
mutableAge: number // This is OK with ignore-prefix = "mutable" | ||
}; | ||
``` | ||
Yes, variable names like `mutableAge` are ugly, but then again mutation is an ugly business :-). | ||
## Sample Configuration File | ||
Here's a sample TSLint configuration file (tslint.json) that activates all the recommended rules: | ||
Here's a sample TSLint configuration file (tslint.json) that activates all the rules: | ||
```json | ||
```javascript | ||
{ | ||
"rulesDirectory": "path/to/tslint-immutable/rules", | ||
"rulesDirectory": ["./node_modules/tslint-immutable/rules"], | ||
"rules": { | ||
// Immutability rules | ||
"readonly-interface": true, | ||
"readonly-indexer": true, | ||
"readonly-array": true, | ||
"no-let": true, | ||
"no-var-keyword": true, // built-in tslint rule | ||
// Functional style rules | ||
"no-this": true, | ||
"no-class": true, | ||
"no-new": true, | ||
"no-mixed-interface": true, | ||
"no-expression-statement": true, | ||
"no-var-keyword": true | ||
// Other rules | ||
"no-arguments": true, | ||
"no-label": true, | ||
"no-semicolon-interface": true, | ||
"import-containment": [ true, | ||
{ | ||
"containmentPath": "path/to/libs", | ||
"allowedExternalFileNames": ["index"], | ||
"disallowedInternalFileNames": ["index"] | ||
}] | ||
} | ||
@@ -221,0 +306,0 @@ } |
@@ -21,4 +21,3 @@ "use strict"; | ||
Rule.prototype.apply = function (sourceFile) { | ||
var walker = new ReadonlyArrayWalker(sourceFile, this.getOptions()); | ||
return this.applyWithWalker(walker); | ||
return this.applyWithFunction(sourceFile, walk, parseOptions(this.ruleArguments)); | ||
}; | ||
@@ -29,32 +28,59 @@ return Rule; | ||
exports.Rule = Rule; | ||
var ReadonlyArrayWalker = (function (_super) { | ||
__extends(ReadonlyArrayWalker, _super); | ||
function ReadonlyArrayWalker() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
var OPTION_IGNORE_LOCAL = "ignore-local"; | ||
var OPTION_IGNORE_PREFIX = "ignore-prefix"; | ||
function parseOptions(options) { | ||
var ignoreLocal = options.indexOf(OPTION_IGNORE_LOCAL) !== -1; | ||
var ignorePrefix; | ||
for (var _i = 0, options_1 = options; _i < options_1.length; _i++) { | ||
var o = options_1[_i]; | ||
if (typeof o === "object" && o[OPTION_IGNORE_PREFIX] != null) { | ||
ignorePrefix = o[OPTION_IGNORE_PREFIX]; | ||
break; | ||
} | ||
} | ||
ReadonlyArrayWalker.prototype.visitTypeReference = function (node) { | ||
_super.prototype.visitTypeReference.call(this, node); | ||
if (node.typeName.getText() === "Array") { | ||
this.addFailure(this.createFailure(node.typeName.getStart(), node.typeName.getWidth(), Rule.FAILURE_STRING)); | ||
return { ignoreLocal: ignoreLocal, ignorePrefix: ignorePrefix }; | ||
} | ||
function walk(ctx) { | ||
return ts.forEachChild(ctx.sourceFile, cb); | ||
function cb(node) { | ||
if (ctx.options.ignoreLocal && (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.ArrowFunction)) { | ||
// skip checking in functions if ignore-local is set | ||
return; | ||
} | ||
}; | ||
ReadonlyArrayWalker.prototype.visitArrayLiteralExpression = function (node) { | ||
_super.prototype.visitArrayLiteralExpression.call(this, node); | ||
// If the array literal is used in a variable declaration, the variable | ||
// must have a type spcecified, otherwise it will implicitly be of mutable Array type | ||
if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclaration) { | ||
if (node.kind === ts.SyntaxKind.TypeReference && isInvalidArrayTypeReference(node, ctx)) { | ||
ctx.addFailureAtNode(node, Rule.FAILURE_STRING); | ||
} | ||
if (node.kind === ts.SyntaxKind.ArrayLiteralExpression && isInvalidArrayLiteralExpression(node, ctx)) { | ||
var variableDeclarationNode = node.parent; | ||
if (!variableDeclarationNode.type) { | ||
this.addFailure(this.createFailure(variableDeclarationNode.name.getStart(), variableDeclarationNode.name.getWidth(), Rule.FAILURE_STRING)); | ||
ctx.addFailureAt(variableDeclarationNode.name.getStart(ctx.sourceFile), variableDeclarationNode.name.getWidth(ctx.sourceFile), Rule.FAILURE_STRING); | ||
} | ||
return ts.forEachChild(node, cb); | ||
} | ||
} | ||
function isInvalidArrayTypeReference(node, ctx) { | ||
if (node.typeName.getText(ctx.sourceFile) === "Array") { | ||
if (ctx.options.ignorePrefix) { | ||
var variableDeclarationNode = node.parent; | ||
if (variableDeclarationNode.name.getText(ctx.sourceFile).substr(0, ctx.options.ignorePrefix.length) === ctx.options.ignorePrefix) { | ||
return false; | ||
} | ||
} | ||
}; | ||
ReadonlyArrayWalker.prototype.visitTypeLiteral = function (node) { | ||
_super.prototype.visitTypeLiteral.call(this, node); | ||
// if (node.kind === ts.SyntaxKind.ArrayType) { | ||
if (node.kind === ts.SyntaxKind.ArrayType) { | ||
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING)); | ||
return true; | ||
} | ||
return false; | ||
} | ||
function isInvalidArrayLiteralExpression(node, ctx) { | ||
// If the array literal is used in a variable declaration, the variable | ||
// must have a type spcecified, otherwise it will implicitly be of mutable Array type | ||
if (node.parent && node.parent.kind === ts.SyntaxKind.VariableDeclaration) { | ||
var variableDeclarationNode = node.parent; | ||
if (!variableDeclarationNode.type) { | ||
if (ctx.options.ignorePrefix && | ||
variableDeclarationNode.name.getText(ctx.sourceFile).substr(0, ctx.options.ignorePrefix.length) === ctx.options.ignorePrefix) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
}; | ||
return ReadonlyArrayWalker; | ||
}(Lint.RuleWalker)); | ||
} | ||
return false; | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
74833
61
861
302
0