Skip to content
This repository was archived by the owner on Feb 21, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ These rules are only relevant to ES6 environments.
|:x:|[prefer-destructuring](http://eslint.org/docs/rules/prefer-destructuring)|prefer-destructuring|require using destructuring when assigning to variables from arrays and objects|
|:x:|[prefer-reflect](http://eslint.org/docs/rules/prefer-reflect)|prefer-reflect|suggest using Reflect methods where applicable|
|:x:|[prefer-rest-params](http://eslint.org/docs/rules/prefer-rest-params)|prefer-rest-params|suggest using the rest parameters instead of `arguments`|
|:x:|[prefer-spread](http://eslint.org/docs/rules/prefer-spread)|prefer-spread|suggest using the spread operator instead of `.apply()`.|
|:white_check_mark:|[prefer-spread](http://eslint.org/docs/rules/prefer-spread)|[ter-prefer-spread](https://github.com/buzinas/tslint-eslint-rules/blob/master/src/docs/rules/terPreferSpreadRule.md)|suggest using the spread operator instead of `.apply()`.|
|:x:|[prefer-template](http://eslint.org/docs/rules/prefer-template)|prefer-template|suggest using template literals instead of strings concatenation|
|:x:|[require-yield](http://eslint.org/docs/rules/require-yield)|require-yield|disallow generator functions that do not have `yield`|
|:x:|[template-curly-spacing](http://eslint.org/docs/rules/template-curly-spacing)|template-curly-spacing|enforce spacing around embedded expressions of template strings|
Expand Down
23 changes: 23 additions & 0 deletions src/docs/rules/terPreferSpreadRule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- Start:AutoDoc:: Modify `src/readme/rules.ts` and run `gulp readme` to update block -->
## ter-prefer-spread (ESLint: [prefer-spread](http://eslint.org/docs/rules/prefer-spread))
[![rule_source](https://img.shields.io/badge/%F0%9F%93%8F%20rule-source-green.svg)](https://github.com/buzinas/tslint-eslint-rules/blob/master/src/rules/terPreferSpreadRule.ts)
[![test_source](https://img.shields.io/badge/%F0%9F%93%98%20test-source-blue.svg)](https://github.com/buzinas/tslint-eslint-rules/blob/master/src/test/rules/terPreferSpreadRuleTests.ts)

suggest using the spread operator instead of `.apply()`.

#### Rationale

This rule is aimed to flag usage of Function.prototype.apply() in situations where the spread operator could be used instead.

### Config


#### Examples


#### Schema

```json
null
```
<!-- End:AutoDoc -->
4 changes: 2 additions & 2 deletions src/readme/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3028,9 +3028,9 @@ const rules: IRule[] = [
~~~`
},
{
available: false,
available: true,
eslintRule: 'prefer-spread',
tslintRule: 'prefer-spread',
tslintRule: 'ter-prefer-spread',
category: 'ECMAScript 6',
description: 'suggest using the spread operator instead of `.apply()`.',
eslintUrl: 'http://eslint.org/docs/rules/prefer-spread',
Expand Down
76 changes: 76 additions & 0 deletions src/rules/terPreferSpreadRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as ts from 'typescript';
import * as Lint from 'tslint';
import { equalTokens, isNullOrUndefined } from '../support/token';

const RULE_NAME = 'ter-prefer-spread';

export class Rule extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
ruleName: RULE_NAME,
hasFix: true,
description: 'require spread operators instead of `.apply()`',
rationale: Lint.Utils.dedent`
This rule is aimed to flag usage of Function.prototype.apply() in situations where the spread operator could be used instead.
`,
optionsDescription: Lint.Utils.dedent`
`,
options: null,
optionExamples: [],
typescriptOnly: false,
type: 'style'
};

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const walker = new RuleWalker(sourceFile, this.getOptions());
return this.applyWithWalker(walker);
}
}

const isVariadicApplyCalling = (node: ts.CallExpression) => (
node.expression.kind === ts.SyntaxKind.PropertyAccessExpression &&
(node.expression as ts.PropertyAccessExpression).name.kind === ts.SyntaxKind.Identifier &&
((node.expression as ts.PropertyAccessExpression).name as ts.Identifier).text === 'apply' &&
node.arguments.length === 2 &&
node.arguments[1].kind !== ts.SyntaxKind.ArrayLiteralExpression &&
node.arguments[1].kind !== ts.SyntaxKind.SpreadElement
);

const isValidThisArg = (expectedThis: null | ts.Node, thisArg: ts.Node) => {
if (expectedThis === null) {
return isNullOrUndefined(thisArg);
}

return equalTokens(expectedThis, thisArg);
};

class RuleWalker extends Lint.RuleWalker {
protected visitCallExpression(node: ts.CallExpression) {
if (!isVariadicApplyCalling(node)) {
return;
}

const applied = (node.expression as ts.PropertyAccessExpression).expression;
const expectedThis = applied.kind === ts.SyntaxKind.PropertyAccessExpression
? (applied as ts.PropertyAccessExpression).expression
: applied.kind === ts.SyntaxKind.ElementAccessExpression
? (applied as ts.ElementAccessExpression).expression
: null;
const thisArg = node.arguments[0];

if (isValidThisArg(expectedThis, thisArg)) {
if (expectedThis !== null && expectedThis.kind !== ts.SyntaxKind.Identifier) {

this.addFailureAtNode(
node,
'Use the spread operator instead of \'.apply()\''
);
} else {
this.addFailureAtNode(
node,
'Use the spread operator instead of \'.apply()\'',
Lint.Replacement.replaceFromTo(node.getStart(), node.getEnd(), `${applied.getText()}(...${node.arguments[1].getText()})`)
);
}
}
}
}
38 changes: 38 additions & 0 deletions src/support/token.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
import * as ts from 'typescript';
import { isTokenKind } from 'tsutils';

export function isAssignmentToken(token: ts.Node) {
return token.kind >= ts.SyntaxKind.FirstAssignment && token.kind <= ts.SyntaxKind.LastAssignment;
}

export function isNullOrUndefined(node: ts.Node) {
return node.kind === ts.SyntaxKind.NullKeyword ||
(node.kind === ts.SyntaxKind.Identifier && (node as ts.Identifier).text === 'undefined') ||
node.kind === ts.SyntaxKind.VoidExpression;
}

export function listTokens(node: ts.Node): ts.Node[] {
if (isTokenKind(node.kind)) {
return [node];
}

if (node.kind !== ts.SyntaxKind.JSDocComment) {
return node
.getChildren(node.getSourceFile())
.filter(child => child.kind !== ts.SyntaxKind.OpenBracketToken && child.kind !== ts.SyntaxKind.CloseBracketToken);
}

return [];
}

export function equalTokens(leftNode: ts.Node, rightNode: ts.Node) {
const tokensL = listTokens(leftNode);
const tokensR = listTokens(rightNode);

if (tokensL.length !== tokensR.length) {
return false;
}

for (let i = 0; i < tokensL.length; ++i) {
if (tokensL[i].getText() !== tokensR[i].getText()) {
return false;
}
}

return true;
}
80 changes: 80 additions & 0 deletions src/test/rules/terPreferSpreadRuleTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Failure, Position, RuleTester } from './ruleTester';

const ruleTester = new RuleTester('ter-prefer-spread', true);

function expecting(lineEnd: number, characterEnd: number, positionEnd: number): Failure[] {
return [{
failure: 'Use the spread operator instead of \'.apply()\'',
startPosition: new Position(0, 0, 0),
endPosition: new Position(lineEnd, characterEnd, positionEnd)
}];
}

ruleTester.addTestGroup('valid', 'should pass when target is different', [
'foo.apply(obj, args);',
'obj.foo.apply(null, args);',
'obj.foo.apply(otherObj, args);',
'a.b(x, y).c.foo.apply(a.b(x, z).c, args);',
'a.b.foo.apply(a.b.c, args);'
]);

ruleTester.addTestGroup('valid', 'should pass when non variadic', [
'foo.apply(undefined, [1, 2]);',
'foo.apply(null, [1, 2]);',
'obj.foo.apply(obj, [1, 2]);'
]);

ruleTester.addTestGroup('valid', 'should pass when property is computed', [
'var apply; foo[apply](null, args);'
]);

ruleTester.addTestGroup('valid', 'should pass when incomplete', [
'foo.apply();',
'obj.foo.apply();',
'obj.foo.apply(obj, ...args)'
]);

ruleTester.addTestGroup('invalid', 'should report an error', [
{
code: 'foo.apply(undefined, args);',
output: 'foo(...args);',
errors: expecting(0, 26, 26)
},
{
code: 'foo.apply(void 0, args);',
output: 'foo(...args);',
errors: expecting(0, 23, 23)
},
{
code: 'foo.apply(null, args);',
output: 'foo(...args);',
errors: expecting(0, 21, 21)
},
{
code: 'obj.foo.apply(obj, args);',
output: 'obj.foo(...args);',
errors: expecting(0, 24, 24)
},
{
// Not fixed: a.b.c might activate getters
code: 'a.b.c.foo.apply(a.b.c, args);',
errors: expecting(0, 28, 28)
},
{
// Not fixed: a.b(x, y).c might activate getters
code: 'a.b(x, y).c.foo.apply(a.b(x, y).c, args);',
errors: expecting(0, 40, 40)
},
{
// Not fixed (not an identifier)
code: '[].concat.apply([ ], args);',
errors: expecting(0, 26, 26)
},
{
// Not fixed (not an identifier)
code: '[].concat.apply([\n/*empty*/\n], args);',
errors: expecting(2, 8, 36)
}
]);

ruleTester.runTests();