Skip to content

Commit

Permalink
Update: Refactor one-dependency-per-line rule
Browse files Browse the repository at this point in the history
* DRY-up code
* Add `meta` information
* Replace custom `unique` function with a `Set`
  • Loading branch information
Casey Visco committed Aug 18, 2016
1 parent b2135b6 commit ca2bb1e
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 175 deletions.
182 changes: 78 additions & 104 deletions lib/rules/one-dependency-per-line.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/**
* @fileoverview Require or disallow one dependency per line.
* @fileoverview Rule to enforce or disallow one dependency per line.
* @author Casey Visco
*/

"use strict";

const unique = require("../utils/unique");
const util = require("../util");
const ast = require("../utils/ast");

Expand All @@ -15,9 +14,62 @@ const isArrayExpr = ast.isArrayExpr;
const isFunctionExpr = ast.isFunctionExpr;
const isStringLiteral = ast.isStringLiteral;

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

const pad = (amount) => (value) => " ".repeat(amount) + value;
const line = (node) => node.loc.start.line;
const column = (node) => node.loc.start.column;
const unique = (list) => Array.from(new Set(list));
const hasDuplicates = (list) => unique(list).length < list.length;
const hasMultiple = (list) => unique(list).length > 1;

const indentation = (node) => {
const statement = node.body.body[0];
return statement && line(node) !== line(statement) ? column(statement) : 0;
};

const formatPaths = (indent) => (node) => {
const paths = node.elements
.map(v => v.raw)
.map(pad(indent))
.join(",\n");

return `[\n${paths}\n]`;
};

const formatNames = (indent, context) => (node) => {
const body = context.getSourceCode().getText(node.body);
const names = node.params
.map(v => v.name)
.map(pad(indent))
.join(",\n");

return `function (\n${names}\n) ${body}`;
};

// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------

const ALWAYS_MSG = {
paths: "Only one dependency path is permitted per line.",
names: "Only one dependency name is permitted per line."
};

const NEVER_MSG = {
paths: "Dependency paths must appear on one line.",
names: "Dependency names must appear on one line."
};

module.exports = {
meta: {
docs: {},
docs: {
description: "Require or disallow one dependency per line",
category: "Stylistic Choices",
recommended: false
},
fixable: "code",
schema: [
{
Expand Down Expand Up @@ -52,107 +104,45 @@ module.exports = {
},

create: function (context) {
const ALWAYS_PATHS_MESSAGE = "Only one dependency path is permitted per line.";
const ALWAYS_NAMES_MESSAGE = "Only one dependency name is permitted per line.";
const NEVER_PATHS_MESSAGE = "Dependency paths must appear on one line.";
const NEVER_NAMES_MESSAGE = "Dependency names must appear on one line.";

const options = context.options[0] || {};

const settings = {
paths: "paths" in options ? options.paths : "always",
names: "names" in options ? options.names : "always"
};

const sourceCode = context.getSourceCode();

function lineNum(node) {
return node.loc.start.line;
}

function hasDuplicateValues(list) {
return unique(list).length < list.length;
}

function hasMultipleValues(list) {
return unique(list).length > 1;
}

function isAlways(setting, list) {
const value = settings[setting];

if (value === "always") {
return true;
}

if (value === "never") {
return false;
switch (value) {
case "always":
return true;
case "never":
return false;
default:
return list.length > value;
}

return list.length > value;
}

function isNever(setting) {
return settings[setting] === "never";
}

function getFileIndentationLevel(args) {
const functionLine = lineNum(args[1]);
const firstStatementLine = lineNum(args[1].body.body[0]);
function check(setting, node, list, format) {
const lines = list.map(line);
const fix = (fixer) => fixer.replaceTextRange(node.range, format(node));

if (functionLine !== firstStatementLine) {
return args[1].body.body[0].loc.start.column;
if (isAlways(setting, lines) && hasDuplicates(lines)) {
context.report({ node, fix, message: ALWAYS_MSG[setting] });
} else if (isNever(setting) && hasMultiple(lines)) {
context.report({ node, message: NEVER_MSG[setting] });
}

return 0;
}

function getPathsFixer(args) {
return function (fixer) {
const indentLevel = isFunctionExpr(args[1]) ? getFileIndentationLevel(args) : 0;
const pathsNode = args[0];
const paths = pathsNode.elements;
let formattedPaths = "";

paths.forEach(function (path) {
formattedPaths += " ".repeat(indentLevel) + path.raw + ",\n";
});

formattedPaths = formattedPaths.slice(0, -2);
let replacerText = "[\n" + formattedPaths + "\n]";

return fixer.replaceTextRange(pathsNode.range, replacerText);
};
}

function getNamesFixer(args) {
return function (fixer) {
const indentLevel = isFunctionExpr(args[1]) ? getFileIndentationLevel(args) : 0;
const namesNode = args[1];
const names = namesNode.params;

let replacerText = "";
let formattedNames = "";

names.forEach(function (name) {
formattedNames += " ".repeat(indentLevel) + name.name + ",\n";
});

formattedNames = formattedNames.slice(0, -2);
replacerText += "function (\n" + formattedNames + "\n)";
replacerText += " " + sourceCode.getText(namesNode.body);

return fixer.replaceTextRange(namesNode.range, replacerText);
};
}

return {
"CallExpression": function (node) {
let args = node.arguments;
let paths = [];
let names = [];

if (!(isDefineCall(node) || isRequireCall(node)) || args.length < 2) {
if (!isDefineCall(node) && !isRequireCall(node) || args.length < 2) {
return;
}

Expand All @@ -161,34 +151,18 @@ module.exports = {
args = args.slice(1);
}

// Get dependency path list
if (isArrayExpr(args[0])) {
paths = args[0].elements;
} else if (isRequireCall(node) && isStringLiteral(args[0])) {
paths = [ args[0] ];
} else {
return;
}
const deps = args[0];
const func = args[1];

// Get dependency alias list
if (isFunctionExpr(args[1])) {
names = args[1].params;
// We can only work with valid AMD-Style require or define calls
if (!isArrayExpr(deps) || !isFunctionExpr(func)) {
return;
}

const pathsLineNums = paths.map(lineNum);
const namesLineNums = names.map(lineNum);
const indent = indentation(args[1]);

if (isAlways("paths", pathsLineNums) && hasDuplicateValues(pathsLineNums)) {
context.report({node: node, message: ALWAYS_PATHS_MESSAGE, fix: getPathsFixer(args, pathsLineNums)});
} else if (isNever("paths") && hasMultipleValues(pathsLineNums)) {
context.report(node, NEVER_PATHS_MESSAGE);
}

if (isAlways("names", namesLineNums) && hasDuplicateValues(namesLineNums)) {
context.report({node: node, message: ALWAYS_NAMES_MESSAGE, fix: getNamesFixer(args, namesLineNums)});
} else if (isNever("names") && hasMultipleValues(namesLineNums)) {
context.report(node, NEVER_NAMES_MESSAGE);
}
check("paths", deps, deps.elements, formatPaths(indent));
check("names", func, func.params, formatNames(indent, context));
}
};
}
Expand Down
10 changes: 5 additions & 5 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function isDefineCall(node) {
* @returns {Boolean} true if represents an AMD style module definition
*/
function isAmdDefine(node) {
const args = node.arguments || [];
const args = node.arguments;
return args.length === 2 && isArrayExpr(args[0]) && isFunctionExpr(args[1]) ||
args.length === 3 && isStringLiteral(args[0]) && isArrayExpr(args[1]) && isFunctionExpr(args[2]);
}
Expand All @@ -97,7 +97,7 @@ function isAmdDefine(node) {
* @returns {Boolean} true if represents an Object Module Definition
*/
function isObjectDefine(node) {
const args = node.arguments || [];
const args = node.arguments;
return args.length === 1 && isObjectExpr(args[0]) ||
args.length === 2 && isStringLiteral(args[0]) && isObjectExpr(args[1]);
}
Expand All @@ -113,7 +113,7 @@ function isObjectDefine(node) {
* @returns {Boolean} true if represents a Simple Function Definition
*/
function isFunctionDefine(node) {
const args = node.arguments || [];
const args = node.arguments;
return args.length === 1 && isSimpleFuncExpr(args[0]) ||
args.length === 2 && isStringLiteral(args[0]) && isSimpleFuncExpr(args[1]);
}
Expand All @@ -129,7 +129,7 @@ function isFunctionDefine(node) {
* @returns {Boolean} true if represents a Simplified CommonJS Wrapper
*/
function isCommonJsWrapper(node) {
const args = node.arguments || [];
const args = node.arguments;
return args.length === 1 && isCommonJsFuncExpr(args[0]) ||
args.length === 2 && isStringLiteral(args[0]) && isCommonJsFuncExpr(args[1]);
}
Expand All @@ -145,7 +145,7 @@ function isCommonJsWrapper(node) {
* @returns {Boolean} true if represents a named module definition function
*/
function isNamedDefine(node) {
const args = node.arguments || [];
const args = node.arguments;

if (args.length < 2 || args.length > 3) {
return false;
Expand Down
12 changes: 0 additions & 12 deletions lib/utils/unique.js

This file was deleted.

8 changes: 4 additions & 4 deletions tests/lib/rules/one-dependency-per-line.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ const rule = require("../../../lib/rules/one-dependency-per-line");

const ALWAYS_PATHS_ERROR = {
message: "Only one dependency path is permitted per line.",
type: "CallExpression"
type: "ArrayExpression"
};

const ALWAYS_NAMES_ERROR = {
message: "Only one dependency name is permitted per line.",
type: "CallExpression"
type: "FunctionExpression"
};

const NEVER_PATHS_ERROR = {
message: "Dependency paths must appear on one line.",
type: "CallExpression"
type: "ArrayExpression"
};

const NEVER_NAMES_ERROR = {
message: "Dependency names must appear on one line.",
type: "CallExpression"
type: "FunctionExpression"
};

testRule("one-dependency-per-line", rule, {
Expand Down
50 changes: 0 additions & 50 deletions tests/lib/utils/unique.js

This file was deleted.

0 comments on commit ca2bb1e

Please sign in to comment.