Skip to content

Commit

Permalink
Merge pull request #9 from ramda/new-rules
Browse files Browse the repository at this point in the history
Detect new code smells
  • Loading branch information
haskellcamargo authored Nov 18, 2017
2 parents 673071b + 6eeb3da commit 2f1f8f8
Show file tree
Hide file tree
Showing 13 changed files with 406 additions and 12 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ Configure it in `package.json`.
"ramda"
],
"rules": {
"ramda/always-simplification": "error",
"ramda/any-pass-simplification": "error",
"ramda/both-simplification": "error",
"ramda/complement-simplification": "error",
"ramda/compose-simplification": "error",
"ramda/cond-simplification": "error",
"ramda/either-simplification": "error",
"ramda/filter-simplification": "error",
Expand All @@ -40,6 +42,8 @@ Configure it in `package.json`.
"ramda/no-redundant-and": "error",
"ramda/no-redundant-not": "error",
"ramda/no-redundant-or": "error",
"ramda/pipe-simplification": "error",
"ramda/prefer-ramda-boolean": "error",
"ramda/prop-satisfies-simplification": "error",
"ramda/reduce-simplification": "error",
"ramda/reject-simplification": "error",
Expand All @@ -53,9 +57,11 @@ Configure it in `package.json`.

## Rules

- `always-simplification` - Detects when `always` usage can be replaced by a Ramda function
- `any-pass-simplification` - Suggests simplifying list of negations in `anyPass` by single negation in `allPass`
- `both-simplification` - Suggests transforming negated `both` conditions on negated `either`
- `complement-simplification` - Forbids confusing `complement`, suggesting a better one
- `compose-simplification` - Detects when a function that has the same behavior already exists
- `cond-simplification` - Forbids using `cond` when `ifElse`, `either` or `both` fits
- `either-simplification` - Suggests transforming negated `either` conditions on negated `both`
- `filter-simplification` - Forbids using negated `filter` and suggests `reject`
Expand All @@ -65,6 +71,8 @@ Configure it in `package.json`.
- `no-redundant-and` - Forbids `and` with 2 parameters in favor of `&&`
- `no-redundant-not` - Forbids `not` with 1 parameter in favor of `!`
- `no-redundant-or` - Forbids `or` with 2 parameters in favor of `||`
- `pipe-simplification` - Detects when a function that has the same behavior already exists
- `prefer-ramda-boolean` - Enforces using `R.T` and `R.F` instead of explicit functions
- `prop-satisfies-simplification` - Detects when can replace `propSatisfies` by more simple functions
- `reduce-simplification` - Detects when can replace `reduce` by `sum` or `product`
- `reject-simplification` - Forbids using negated `reject` and suggests `filter`
Expand Down
7 changes: 7 additions & 0 deletions ast-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ const isCalling = pattern => R.where({
arguments: pattern.arguments || R.T
});

// :: Node -> Boolean
const isBooleanLiteral = R.both(
R.propEq('type', 'Literal'),
R.propSatisfies(R.is(Boolean), 'value')
);

exports.isRamdaMethod = isRamdaMethod;
exports.isCalling = isCalling;
exports.getName = getName;
exports.isBooleanLiteral = isBooleanLiteral;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-ramda",
"version": "2.1.0",
"version": "2.2.0",
"description": "ESLint rules for use with Ramda",
"license": "MIT",
"keywords": [
Expand Down
45 changes: 45 additions & 0 deletions rules/always-simplification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';
const R = require('ramda');
const ast = require('../ast-helper');

const isCalling = ast.isCalling;
const isBooleanLiteral = ast.isBooleanLiteral;

const report = (instead, prefer) => `\`always(${instead})\` should be simplified to \`${prefer}\``;
const alternatives = {
'true': 'T',
'false': 'F'
};

const create = context => ({
CallExpression(node) {
const match = isCalling({
name: 'always',
arguments: R.both(
R.propEq('length', 1),
R.where({
0: isBooleanLiteral
})
)
});

if (match(node)) {
const instead = node.arguments[0].value;

context.report({
node,
message: report(instead, alternatives[instead])
});
}
}
});

module.exports = {
create,
meta: {
docs: {
description: 'Detects cases where `always` is redundant',
recommended: 'off'
}
}
};
38 changes: 38 additions & 0 deletions rules/compose-simplification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';
const R = require('ramda');
const ast = require('../ast-helper');

const isCalling = ast.isCalling;
const isRamdaMethod = ast.isRamdaMethod;

const create = context => ({
CallExpression(node) {
const match = isCalling({
name: 'compose',
arguments: R.both(
R.propSatisfies(R.lte(2), 'length'),
R.where({
0: isRamdaMethod('flatten'),
1: isRamdaMethod('map')
})
)
});

if (match(node)) {
context.report({
node,
message: '`compose(flatten, map)` should be simplified to `chain`'
});
}
}
});

module.exports = {
create,
meta: {
docs: {
description: 'Detects when there are better functions that behave the same as `compose`',
recommended: 'off'
}
}
};
19 changes: 14 additions & 5 deletions rules/map-simplification.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
'use strict';
const R = require('ramda');
const isCalling = require('../ast-helper').isCalling;
const ast = require('../ast-helper');

const isCalling = ast.isCalling;
const getName = ast.getName;

const create = context => ({
CallExpression(node) {
const match = isCalling({
name: 'map',
arguments: R.both(
R.propSatisfies(R.lt(0), 'length'),
R.propSatisfies(isCalling({
name: 'prop'
}), 0)
R.propSatisfies(R.either(
isCalling({ name: 'prop' }),
isCalling({ name: 'pickAll' })
), 0)
)
});

if (match(node)) {
const map = {
prop: 'pluck',
pickAll: 'project'
};
const callee = getName(node.arguments[0].callee);
context.report({
node,
message: '`map(prop(_))` should be simplified to `pluck(_)`'
message: `\`map(${callee}(_))\` should be simplified to \`${map[callee]}(_)\``
});
}
}
Expand Down
38 changes: 38 additions & 0 deletions rules/pipe-simplification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';
const R = require('ramda');
const ast = require('../ast-helper');

const isCalling = ast.isCalling;
const isRamdaMethod = ast.isRamdaMethod;

const create = context => ({
CallExpression(node) {
const match = isCalling({
name: 'pipe',
arguments: R.both(
R.propSatisfies(R.lte(2), 'length'),
R.where({
0: isRamdaMethod('map'),
1: isRamdaMethod('flatten')
})
)
});

if (match(node)) {
context.report({
node,
message: '`pipe(map, flatten)` should be simplified to `chain`'
});
}
}
});

module.exports = {
create,
meta: {
docs: {
description: 'Detects when there are better functions that behave the same as `pipe`',
recommended: 'off'
}
}
};
64 changes: 64 additions & 0 deletions rules/prefer-ramda-boolean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';
const R = require('ramda');
const isBooleanLiteral = require('../ast-helper').isBooleanLiteral;

const report = (instead, prefer) => `Instead of \`() => ${instead}\`, prefer \`${prefer}\``;
const getAlternative = R.applyTo(R.__, R.compose(R.toUpper, R.head, R.toString));

// :: Node -> Boolean
const onlyReturnsBoolean = R.where({
type: R.equals('BlockStatement'),
body: R.both(
R.propEq('length', 1),
R.where({
0: R.where({
type: R.equals('ReturnStatement'),
argument: R.both(
R.complement(R.isNil),
isBooleanLiteral
)
})
})
)
});

// Node -> String
const getRawReturn = R.ifElse(
R.propEq('type', 'BlockStatement'),
R.path(['body', 0, 'argument', 'value']),
R.prop('value')
);

const create = context => ({
ArrowFunctionExpression(node) {
const match = R.either(isBooleanLiteral, onlyReturnsBoolean);

if (match(node.body)) {
const instead = getRawReturn(node.body);
context.report({
node,
message: report(instead, getAlternative(instead))
});
}
},

FunctionExpression(node) {
if (onlyReturnsBoolean(node.body)) {
const instead = node.body.body[0].argument.value;
context.report({
node,
message: report(instead, getAlternative(instead))
});
}
}
});

module.exports = {
create,
meta: {
docs: {
description: 'Suggests using Ramda T and F functions instead of explicit versions',
recommended: 'error'
}
}
};
35 changes: 35 additions & 0 deletions test/always-simplification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import test from 'ava';
import avaRuleTester from 'eslint-ava-rule-tester';
import rule from '../rules/always-simplification';

const ruleTester = avaRuleTester(test, {
env: {
es6: true
},
parserOptions: {
sourceType: 'module'
}
});

const error = (from, to) => ({
ruleId: 'always-simplification',
message: `\`always(${from})\` should be simplified to \`${to}\``
});

ruleTester.run('always-simplification', rule, {
valid: [
'always',
'always(1)',
'always(always)'
],
invalid: [
{
code: 'always(true)',
errors: [error('true', 'T')]
},
{
code: 'always(false)',
errors: [error('false', 'F')]
}
]
});
37 changes: 37 additions & 0 deletions test/compose-simplification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test from 'ava';
import avaRuleTester from 'eslint-ava-rule-tester';
import rule from '../rules/compose-simplification';

const ruleTester = avaRuleTester(test, {
env: {
es6: true
},
parserOptions: {
sourceType: 'module'
}
});

const error = {
chain: {
ruleId: 'compose-simplification',
message: '`compose(flatten, map)` should be simplified to `chain`'
}
};

ruleTester.run('compose-simplification', rule, {
valid: [
'compose(map, flatten)',
'compose()',
'compose(left, right)'
],
invalid: [
{
code: 'compose(flatten, map)',
errors: [error.chain]
},
{
code: 'R[\'compose\'](R.flatten, map)',
errors: [error.chain]
}
]
});
Loading

0 comments on commit 2f1f8f8

Please sign in to comment.