Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added autofix function for operator-whitespace rule #270

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
269 changes: 178 additions & 91 deletions lib/rules/operator-whitespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

"use strict";

const solparse = require("solparse");

module.exports = {

meta: {
Expand All @@ -15,7 +17,9 @@ module.exports = {
description: "Ensure that operators are surrounded by a single space on either side"
},

schema: []
schema: [],

fixable: "whitespace"

},

Expand All @@ -24,56 +28,90 @@ module.exports = {
let sourceCode = context.getSourceCode();

function inspectAssignmentExpression(emitted) {
/**
* node.operator is refined here by adding backslash before all the 'special' characters.
* 'special' chars are thos chars that are part of solidity assignment operators and, if used without backslash in JS RegExp,
* behave as wildcard characters. So to make sure they're treated as simple strings, we add '\' before them.
* As of today, these chars include: * / + | ^
*/
let node = emitted.node,
op = node.operator.replace(/([\+\*\/\|\^])/g, "\\$1"), opLength = node.operator.length;

const node = emitted.node;
if (emitted.exit) {
return;
}

// If expression is 'abc *= def;', then charsAfterLeftNode will contain ' *= d'.
let charsAfterLeftNode = sourceCode.getNextChars(node.left, 3 + opLength),
validationRegexp = new RegExp("^ " + op + " [^\\s]$");

(!validationRegexp.test(charsAfterLeftNode)) && context.report({
node: node.left,
message: "Assignment operator must have exactly single space on both sides of it."
});
const code = sourceCode.getStringBetweenNodes(node.left, node.right);
const escapedCode = commentsToWhitespace(code);
const [leftOfOperator, rightOfOperator] = escapedCode.split(node.operator);

if (leftOfOperator !== " " || rightOfOperator !== " ") {
context.report({
node: node,
message: "Assignment operator must have exactly single space on both sides of it.",
fix(fixer) {
return fixer.replaceTextRange(
[node.left.end, node.right.start],
` ${node.operator} `
);
}
});
}
}

//statement like `var x = 10` doesn't come under AssignmentExpression, so needs to be checked separately
function inspectVariableDeclaration(emitted) {
let node = emitted.node, code = sourceCode.getText(node);
const node = emitted.node;

if (emitted.exit) {
return;
}

//if a particular character is '=', check its left and right for single space
for (let i = 2; i < code.length; i++) {
if (code [i] === "=") {
(!/^[^\/\s] $/.test(code.slice(i-2, i))) && context.report({
node: node,
message: "There should be only a single space between assignment operator '=' and its left side."
});
const escapedCode = commentsToWhitespace(sourceCode.getText(node));

(!/^ [^\/\s]$/.test(code.slice(i+1, i+3))) && context.report({
node: node,
message: "There should be only a single space between assignment operator '=' and its right side."
});
//if a particular character is '=', check its left and right for single space
const oneSpaceOnLeft = /^[^\/\s] $/;
const whitespaceOnLeft = /(\s+)$/;
const oneSpaceOnRight = /^ [^\/\s]$/;
const whitespaceOnRight = /^(\s+)/;

for (let i = 2; i < escapedCode.length; i++) {
if (escapedCode [i] === "=") {
if (!oneSpaceOnLeft.test(escapedCode.slice(i - 2, i))) {
context.report({
node: node,
message: "There should be only a single space between assignment operator '=' and its left side.",
fix(fixer) {
const match = escapedCode.slice(0, i).match(whitespaceOnLeft);
if (!match) {
return fixer.insertTextAt(node.start + i, " ");
}

const whitespace = match[1];
const start = node.start + i - whitespace.length;
const end = node.start + i;

return fixer.replaceTextRange([start, end], " ");
}
});
}

if (!oneSpaceOnRight.test(escapedCode.slice(i + 1, i + 3))) {
context.report({
node: node,
message: "There should be only a single space between assignment operator '=' and its right side.",
fix(fixer) {
const match = escapedCode.slice(i + 1, escapedCode.length - 1).match(whitespaceOnRight);
if (!match) {
return fixer.insertTextAt(node.start + i + 1, " ");
}

const whitespace = match[1];
const start = node.start + i + 1;
const end = start + whitespace.length;

return fixer.replaceTextRange([start, end], " ");
}
});
}
}
}
}

function inspectBinaryExpression(emitted) {
let leftNode,
node = emitted.node;
let node = emitted.node;

if (emitted.exit) {
return;
Expand All @@ -86,108 +124,157 @@ module.exports = {
// 1. take line no. of both left & right expr. Line no (right) = line (left) + 1
// Take string btw them, should be NO \n before operator. that's it

let rightNodeStartingLine = sourceCode.getLine(node.right),
leftNodeEndingLine = sourceCode.getEndingLine(node.left),
opRegExp = node.operator.replace(/([\+\*\/\|\^])/g, "\\$1");
const rightNodeStartingLine = sourceCode.getLine(node.right);
const leftNodeEndingLine = sourceCode.getEndingLine(node.left);
const opRegExp = node.operator.replace(/([\+\*\/\|\^])/g, "\\$1");

if (rightNodeStartingLine > leftNodeEndingLine) {
let validationRegexOpOnSameLineAsLeftNodeEnd = new RegExp("^[^\\n]*" + opRegExp),
strBetweenLeftAndRightNode = sourceCode.getStringBetweenNodes(node.left, node.right);
const validationRegexOpOnSameLineAsLeftNodeEnd = new RegExp("^[^\\n]*" + opRegExp);
const strBetweenLeftAndRightNode = sourceCode.getStringBetweenNodes(node.left, node.right);

if (rightNodeStartingLine !== leftNodeEndingLine + 1) {
if (!validationRegexOpOnSameLineAsLeftNodeEnd.test(strBetweenLeftAndRightNode)) {
context.report({
node: node,
location: {
column: sourceCode.getColumn(node.right),
line: sourceCode.getLine(node.right)
column: sourceCode.getEndingColumn(node.left)
},
message: "In Binary Expressions that span over multiple lines, expression on the right side of the operator (" + node.operator + ") must be exactly 1 line below the line on which the left expression ends."
message: "Operator \"" + node.operator + "\" should be on the line where left side of the Binary expression ends.",
fix(fixer) {
// Reduce whitespace to the left of operator to 1 space
// Insert a linebreak if there are no linebreaks to the right

const operator = sourceCode.getStringBetweenNodes(node.left, node.right);
const match = operator.match(/^(\s*)/);
if (!match) {
return null;
}
const whitespace = match[1];

const start = node.left.end;
const end = node.left.end + whitespace.length;

const fixes = [fixer.replaceTextRange([start, end], " ")];

const rightWhitespace = operator.slice(
whitespace.length + node.operator.length,
operator.length
);
if (!rightWhitespace.includes("\n")) {
const operatorEnd = end + node.operator.length;
fixes.push(fixer.insertTextAt(operatorEnd, "\n"));
}

return fixes;
}
});
}

if (!validationRegexOpOnSameLineAsLeftNodeEnd.test(strBetweenLeftAndRightNode)) {
if (rightNodeStartingLine !== leftNodeEndingLine + 1) {
context.report({
node: node,
location: {
column: sourceCode.getEndingColumn(node.left)
column: sourceCode.getColumn(node.right),
line: sourceCode.getLine(node.right)
},
message: "Operator \"" + node.operator + "\" should be on the line where left side of the Binary expression ends."
message: "In Binary Expressions that span over multiple lines, expression on the right side of the operator (" + node.operator + ") must be exactly 1 line below the line on which the left expression ends.",
fix(fixer) {
const operator = sourceCode.getStringBetweenNodes(node.left, node.right);
const match = operator.match(/(\s*)$/);
if (!match) {
return null;
}
const whitespace = match[1];

const start = node.right.start - whitespace.length;
const end = node.right.start;

return fixer.replaceTextRange([start, end], "\n");
}
});
}

return;
}

// Handle case where left node is a binary expression and right node may be a literal
if (sourceCode.isASTNode(node.left) && node.left.type === "BinaryExpression"){
leftNode = node.left.right;
} else {
leftNode = node.left;
}

let strBetweenLeftAndRight = sourceCode.getStringBetweenNodes(leftNode, node.right).split(node.operator),
onlyCharsRegExp = /^[^\s\/]$/;
const code = sourceCode.getStringBetweenNodes(node.left, node.right);
const escapedCode = commentsToWhitespace(code);
const [leftOfOperator, rightOfOperator] = escapedCode.split(node.operator);

// Force spacing based on the left side of operator
const forceNoSpacing = leftOfOperator === "";
const removeRight = forceNoSpacing && rightOfOperator.length > 0;
const insertSpaceOnRight = !forceNoSpacing && rightOfOperator === "";

if (removeRight || insertSpaceOnRight) {
context.report({
node: node,
location: {
column: sourceCode.getEndingColumn(node.left) + 1
},
message: "Single space should be either on both sides of '" + node.operator + "' or not at all.",
fix(fixer) {
if (removeRight) {
const endOfOperator = node.left.end + node.operator.length;

return fixer.removeRange([endOfOperator, endOfOperator + 1]);
}

return fixer.insertTextAt(node.right.start, " ");
}
});
}

if (strBetweenLeftAndRight [0].slice(-1) === " " || strBetweenLeftAndRight [1] [0] === " ") {
if (strBetweenLeftAndRight [0].slice(-1) !== strBetweenLeftAndRight [1] [0]) {
if (!forceNoSpacing) {
if (leftOfOperator.length > 0 && leftOfOperator !== " ") {
context.report({
node: node,
location: {
column: sourceCode.getEndingColumn(node.left) + 1
},
message: "Single space should be either on both sides of '" + node.operator + "' or not at all."
});
} else {
let secondLastCharOnLeft = strBetweenLeftAndRight [0].slice(-2, -1),
secondCharOnRight = strBetweenLeftAndRight [1] [1];

secondLastCharOnLeft && (!onlyCharsRegExp.test(secondLastCharOnLeft)) && context.report({
node: node,
location: {
column: sourceCode.getEndingColumn(node.left)
},
message: "There should be a maximum of single space and no comments between left side and '" + node.operator + "'."
message: "There should be a maximum of single space and no comments between left side and '" + node.operator + "'.",
fix(fixer) {
const start = node.left.end;
const end = start + leftOfOperator.length;

return fixer.replaceTextRange([start, end], " ");
}
});
}

secondCharOnRight && (!onlyCharsRegExp.test(secondCharOnRight)) && context.report({
if (rightOfOperator.length > 0 && rightOfOperator !== " ") {
context.report({
node: node,
location: {
column: sourceCode.getColumn(node.right)
},
message: "There should be a maximum of single space and no comments between right side and '" + node.operator + "'."
message: "There should be a maximum of single space and no comments between right side and '" + node.operator + "'.",
fix(fixer) {
const start = node.right.start - rightOfOperator.length;
const end = node.right.start;

return fixer.replaceTextRange([start, end], " ");
}
});
}

return;
}

let firstCharOnLeft = strBetweenLeftAndRight [0].slice(-1),
firstCharOnRight = strBetweenLeftAndRight [1] [0];

firstCharOnLeft && (!onlyCharsRegExp.test(firstCharOnLeft)) && context.report({
node: node,
location: {
column: sourceCode.getEndingColumn(node.left)
},
message: "There should be no comments between left side and '" + node.operator + "'."
});

firstCharOnRight && (!onlyCharsRegExp.test(firstCharOnRight)) && context.report({
node: node,
location: {
column: sourceCode.getColumn(node.right)
},
message: "There should be no comments between right side and '" + node.operator + "'."
});

}

return {
BinaryExpression: inspectBinaryExpression,
VariableDeclaration: inspectVariableDeclaration,
AssignmentExpression: inspectAssignmentExpression
};

}

};

function commentsToWhitespace(code) {
const comments = solparse.parseComments(code);
comments.forEach(comment => {
code = code.replace(comment.text, " ".repeat(comment.text.length));
});

return code;
}
Empty file added lib/utils/string-utils.js
Empty file.
Loading