eslint-plugin-grules
Advanced tools
Comparing version 0.0.14 to 0.0.15
@@ -5,3 +5,3 @@ { | ||
"license": "MIT", | ||
"version": "0.0.14", | ||
"version": "0.0.15", | ||
"type": "commonjs", | ||
@@ -12,3 +12,6 @@ "main": "./src/index.js", | ||
}, | ||
"scripts": {}, | ||
"scripts": { | ||
"test": "eslint .", | ||
"test:fix": "eslint --fix ." | ||
}, | ||
"dependencies": { | ||
@@ -23,2 +26,5 @@ "eslint": "^8.54.0", | ||
}, | ||
"devDependencies": { | ||
"eslint-plugin-grules": "^0.0.14" | ||
}, | ||
"repository": { | ||
@@ -25,0 +31,0 @@ "type": "git", |
@@ -47,7 +47,5 @@ module.exports = { | ||
// Conventions that might as well be considered language features | ||
"no-duplicate-imports": "error", | ||
"no-extend-native": "error", | ||
"no-new-native-nonconstructor": "error", | ||
"no-new-wrappers": "error", | ||
"no-unmodified-loop-condition": "error", | ||
"no-unreachable-loop": "error", | ||
"no-unused-private-class-members": "error", | ||
@@ -68,6 +66,6 @@ "class-methods-use-this": "error", | ||
"no-constant-binary-expression": "error", | ||
"no-duplicate-imports": "error", | ||
"no-else-return": ["error", { allowElseIf: false }], | ||
"no-empty": ["error", { allowEmptyCatch: true }], | ||
"no-empty-static-block": "error", | ||
"no-extend-native": "error", | ||
"no-extra-bind": "error", | ||
@@ -80,2 +78,4 @@ "no-array-constructor": "error", | ||
"no-unneeded-ternary": "error", | ||
"no-unmodified-loop-condition": "error", | ||
"no-unreachable-loop": "error", | ||
"no-useless-call": "error", | ||
@@ -114,3 +114,2 @@ "no-useless-computed-key": "error", | ||
"grules/prefer-index-access": "error", | ||
"grules/prefer-string-length-comparison": "error", | ||
@@ -186,4 +185,3 @@ // Unicorn conventions | ||
"prefer-index-access": require("./rules/prefer-index-access.js"), | ||
"prefer-string-length-comparison": require("./rules/prefer-string-length-comparison.js"), | ||
}, | ||
}; |
@@ -5,5 +5,5 @@ module.exports = { | ||
}, | ||
create: function (context) { | ||
create: (context) => { | ||
return { | ||
CallExpression(node) { | ||
CallExpression: (node) => { | ||
if ( | ||
@@ -16,12 +16,8 @@ node.callee.type === "MemberExpression" && | ||
.getText(node.callee.object); | ||
const argument = node.arguments[0]; | ||
let replacement; | ||
const [argument] = node.arguments; | ||
if (argument && argument.type === "Literal") { | ||
replacement = `${objectText}[${argument.raw}]`; | ||
} else { | ||
replacement = `${objectText}[${context | ||
.getSourceCode() | ||
.getText(argument)}]`; | ||
} | ||
const replacement = | ||
argument && argument.type === "Literal" | ||
? `${objectText}[${argument.raw}]` | ||
: `${objectText}[${context.getSourceCode().getText(argument)}]`; | ||
@@ -31,3 +27,3 @@ context.report({ | ||
message: "Use bracket notation instead of .charAt()", | ||
fix(fixer) { | ||
fix: (fixer) => { | ||
return fixer.replaceText(node, replacement); | ||
@@ -34,0 +30,0 @@ }, |
@@ -30,39 +30,4 @@ /** | ||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: "prefer arrow functions", | ||
category: "emcascript6", | ||
recommended: false, | ||
}, | ||
fixable: "code", | ||
schema: [ | ||
{ | ||
type: "object", | ||
properties: { | ||
disallowPrototype: { | ||
type: "boolean", | ||
}, | ||
singleReturnOnly: { | ||
type: "boolean", | ||
}, | ||
classPropertiesAllowed: { | ||
type: "boolean", | ||
}, | ||
allowStandaloneDeclarations: { | ||
type: "boolean", | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create: (context) => ({ | ||
"FunctionDeclaration:exit": (node) => inspectNode(node, context), | ||
"FunctionExpression:exit": (node) => inspectNode(node, context), | ||
}), | ||
}; | ||
const isPrototypeAssignment = (node) => { | ||
let parent = node.parent; | ||
let { parent } = node; | ||
@@ -72,4 +37,5 @@ while (parent) { | ||
case "MemberExpression": | ||
if (parent.property && parent.property.name === "prototype") | ||
if (parent.property && parent.property.name === "prototype") { | ||
return true; | ||
} | ||
parent = parent.object; | ||
@@ -82,3 +48,3 @@ break; | ||
case "ObjectExpression": | ||
parent = parent.parent; | ||
({ parent } = parent.parent); | ||
break; | ||
@@ -94,3 +60,3 @@ default: | ||
const isConstructor = (node) => { | ||
let parent = node.parent; | ||
const { parent } = node; | ||
return parent && parent.kind === "constructor"; | ||
@@ -100,10 +66,19 @@ }; | ||
const containsThis = (node) => { | ||
if (typeof node !== "object" || node === null) return false; | ||
if (node.type === "FunctionDeclaration") return false; | ||
if (node.type === "FunctionExpression") return false; | ||
if (node.type === "ThisExpression") return true; | ||
if (typeof node !== "object" || node === null) { | ||
return false; | ||
} | ||
if (node.type === "FunctionDeclaration") { | ||
return false; | ||
} | ||
if (node.type === "FunctionExpression") { | ||
return false; | ||
} | ||
if (node.type === "ThisExpression") { | ||
return true; | ||
} | ||
return Object.keys(node).some((field) => { | ||
if (field === "parent") { | ||
return false; | ||
} else if (Array.isArray(node[field])) { | ||
} | ||
if (Array.isArray(node[field])) { | ||
return node[field].some(containsThis); | ||
@@ -115,113 +90,67 @@ } | ||
const isNamed = (node) => | ||
node.type === "FunctionDeclaration" && node.id && node.id.name; | ||
const isNamed = (node) => { | ||
return node.type === "FunctionDeclaration" && node.id && node.id.name; | ||
}; | ||
const functionOnlyContainsReturnStatement = (node) => | ||
node.body.body.length === 1 && node.body.body[0].type === "ReturnStatement"; | ||
const functionOnlyContainsReturnStatement = (node) => { | ||
return ( | ||
node.body.body.length === 1 && node.body.body[0].type === "ReturnStatement" | ||
); | ||
}; | ||
const isNamedDefaultExport = (node) => | ||
node.id && node.id.name && node.parent.type === "ExportDefaultDeclaration"; | ||
const isNamedDefaultExport = (node) => { | ||
return ( | ||
node.id && node.id.name && node.parent.type === "ExportDefaultDeclaration" | ||
); | ||
}; | ||
const isClassMethod = (node) => node.parent.type === "MethodDefinition"; | ||
const isClassMethod = (node) => { | ||
return node.parent.type === "MethodDefinition"; | ||
}; | ||
const isGeneratorFunction = (node) => node.generator === true; | ||
const isGeneratorFunction = (node) => { | ||
return node.generator === true; | ||
}; | ||
const isGetterOrSetter = (node) => | ||
node.parent.kind === "set" || node.parent.kind === "get"; | ||
const isGetterOrSetter = (node) => { | ||
return node.parent.kind === "set" || node.parent.kind === "get"; | ||
}; | ||
const isCommonJSModuleProp = (node, name = "module") => | ||
node && | ||
node.type === "MemberExpression" && | ||
node.object && | ||
node.object.type === "Identifier" && | ||
node.object.name === name; | ||
const isCommonJSModuleProp = (node, name = "module") => { | ||
return ( | ||
node && | ||
node.type === "MemberExpression" && | ||
node.object && | ||
node.object.type === "Identifier" && | ||
node.object.name === name | ||
); | ||
}; | ||
const isModuleExport = (node) => | ||
node.parent.type === "AssignmentExpression" && | ||
(isCommonJSModuleProp(node.parent.left) || | ||
isCommonJSModuleProp(node.parent.left, "exports") || | ||
isCommonJSModuleProp(node.parent.left.object)); | ||
const isModuleExport = (node) => { | ||
return ( | ||
node.parent.type === "AssignmentExpression" && | ||
(isCommonJSModuleProp(node.parent.left) || | ||
isCommonJSModuleProp(node.parent.left, "exports") || | ||
isCommonJSModuleProp(node.parent.left.object)) | ||
); | ||
}; | ||
const isStandaloneDeclaration = (node) => | ||
node.type === "FunctionDeclaration" && | ||
(!node.parent || | ||
node.parent.type === "Program" || | ||
node.parent.type === "ExportNamedDeclaration" || | ||
node.parent.type === "ExportDefaultDeclaration"); | ||
const isStandaloneDeclaration = (node) => { | ||
return ( | ||
node.type === "FunctionDeclaration" && | ||
(!node.parent || | ||
node.parent.type === "Program" || | ||
node.parent.type === "ExportNamedDeclaration" || | ||
node.parent.type === "ExportDefaultDeclaration") | ||
); | ||
}; | ||
const inspectNode = (node, context) => { | ||
const opts = context.options[0] || {}; | ||
const tokenStart = (token) => { | ||
return token.start === undefined ? token.range[0] : token.start; | ||
}; | ||
if (isConstructor(node)) return; | ||
if ( | ||
!isClassMethod(node) && | ||
(containsThis(node.params) || containsThis(node.body)) | ||
) | ||
return; | ||
if (isGeneratorFunction(node)) return; | ||
if (isGetterOrSetter(node)) return; | ||
if (isClassMethod(node) && !opts.classPropertiesAllowed) return; | ||
if ( | ||
opts.allowStandaloneDeclarations && | ||
(isStandaloneDeclaration(node) || isModuleExport(node)) | ||
) | ||
return; | ||
if (opts.singleReturnOnly) { | ||
if ( | ||
functionOnlyContainsReturnStatement(node) && | ||
!isNamedDefaultExport(node) && | ||
(opts.classPropertiesAllowed || !isClassMethod(node)) | ||
) | ||
return context.report({ | ||
node, | ||
message: | ||
"Prefer using arrow functions over plain functions which only return a value", | ||
fix(fixer) { | ||
const src = context.getSourceCode(); | ||
let newText = null; | ||
if (node.type === "FunctionDeclaration") { | ||
newText = fixFunctionDeclaration(src, node); | ||
} else if (node.type === "FunctionExpression") { | ||
newText = fixFunctionExpression(src, node); | ||
// In the case of an async method definition, we remove the "async" prefix | ||
if (node.async && node.parent.type === "MethodDefinition") { | ||
const parentTokens = src.getTokens(node.parent); | ||
const asyncToken = parentTokens.find( | ||
tokenMatcher("Identifier", "async") | ||
); | ||
const nextToken = parentTokens.find( | ||
(_, i, arr) => arr[i - 1] && arr[i - 1] === asyncToken | ||
); | ||
return [ | ||
fixer.replaceText(node, newText), | ||
fixer.replaceTextRange( | ||
[tokenStart(asyncToken), tokenStart(nextToken)], | ||
"" | ||
), | ||
]; | ||
} | ||
} | ||
if (newText !== null) { | ||
return fixer.replaceText(node, newText); | ||
} | ||
}, | ||
}); | ||
} else if (opts.disallowPrototype || !isPrototypeAssignment(node)) { | ||
return context.report( | ||
node, | ||
isNamed(node) | ||
? "Use const or class constructors instead of named functions" | ||
: "Prefer using arrow functions over plain functions" | ||
); | ||
} | ||
const tokenEnd = (token) => { | ||
return token.end === undefined ? token.range[1] : token.end; | ||
}; | ||
const tokenStart = (token) => | ||
token.start === undefined ? token.range[0] : token.start; | ||
const tokenEnd = (token) => | ||
token.end === undefined ? token.range[1] : token.end; | ||
const replaceTokens = (origSource, tokens, replacements) => { | ||
@@ -234,5 +163,5 @@ let removeNextLeadingSpace = false; | ||
if (lastTokenEnd >= 0) { | ||
let between = origSource.substring(lastTokenEnd, tokenStart(token)); | ||
let between = origSource.slice(lastTokenEnd, tokenStart(token)); | ||
if (removeNextLeadingSpace) { | ||
between = between.replace(/^\s+/, ""); | ||
between = between.replace(/^\s+/u, ""); | ||
} | ||
@@ -245,8 +174,8 @@ result += between; | ||
if (replaceInfo[2]) { | ||
result = result.replace(/\s+$/, ""); | ||
result = result.replace(/\s+$/u, ""); | ||
} | ||
result += replaceInfo[0]; | ||
removeNextLeadingSpace = !!replaceInfo[1]; | ||
removeNextLeadingSpace = Boolean(replaceInfo[1]); | ||
} else { | ||
result += origSource.substring(tokenStart(token), tokenEnd(token)); | ||
result += origSource.slice(tokenStart(token), tokenEnd(token)); | ||
} | ||
@@ -258,7 +187,10 @@ lastTokenEnd = tokenEnd(token); | ||
const tokenMatcher = | ||
(type, value = undefined) => | ||
(token) => | ||
token.type === type && | ||
(typeof value === "undefined" || token.value === value); | ||
const tokenMatcher = (type, value) => { | ||
return (token) => { | ||
return ( | ||
token.type === type && | ||
(typeof value === "undefined" || token.value === value) | ||
); | ||
}; | ||
}; | ||
@@ -270,3 +202,3 @@ const fixFunctionExpression = (src, node) => { | ||
let swap = {}; | ||
const swap = {}; | ||
const fnKeyword = tokens.find(tokenMatcher("Keyword", "function")); | ||
@@ -306,11 +238,12 @@ let prefix = ""; | ||
const returnRange = node.body.body.find( | ||
(n) => n.type === "ReturnStatement" | ||
).range; | ||
const semicolon = bodyTokens.find( | ||
(t) => | ||
tokenEnd(t) == returnRange[1] && | ||
const returnRange = node.body.body.find((n) => { | ||
return n.type === "ReturnStatement"; | ||
}).range; | ||
const semicolon = bodyTokens.find((t) => { | ||
return ( | ||
tokenEnd(t) === returnRange[1] && | ||
t.value === ";" && | ||
t.type === "Punctuator" | ||
); | ||
); | ||
}); | ||
if (semicolon) { | ||
@@ -325,3 +258,3 @@ swap[tokenStart(semicolon)] = [parens ? ")" : "", true]; | ||
prefix + | ||
replaceTokens(orig, tokens, swap).replace(/ $/, "") + | ||
replaceTokens(orig, tokens, swap).replace(/ $/u, "") + | ||
(parens && !semicolon ? ")" : "") + | ||
@@ -336,3 +269,3 @@ suffix | ||
const bodyTokens = src.getTokens(node.body); | ||
let swap = {}; | ||
const swap = {}; | ||
const asyncKeyword = node.async ? "async " : ""; | ||
@@ -358,3 +291,3 @@ const omitVar = | ||
const functionKeywordToken = tokens.find( | ||
tokenMatcher("Keyword", "function") | ||
tokenMatcher("Keyword", "function"), | ||
); | ||
@@ -375,11 +308,12 @@ const nameToken = src.getTokenAfter(functionKeywordToken); | ||
const returnRange = node.body.body.find( | ||
(n) => n.type === "ReturnStatement" | ||
).range; | ||
const semicolon = bodyTokens.find( | ||
(t) => | ||
tokenEnd(t) == returnRange[1] && | ||
const returnRange = node.body.body.find((n) => { | ||
return n.type === "ReturnStatement"; | ||
}).range; | ||
const semicolon = bodyTokens.find((t) => { | ||
return ( | ||
tokenEnd(t) === returnRange[1] && | ||
t.value === ";" && | ||
t.type === "Punctuator" | ||
); | ||
); | ||
}); | ||
if (semicolon) { | ||
@@ -393,5 +327,127 @@ swap[tokenStart(semicolon)] = [parens ? ")" : "", true]; | ||
return ( | ||
replaceTokens(orig, tokens, swap).replace(/ $/, "") + | ||
replaceTokens(orig, tokens, swap).replace(/ $/u, "") + | ||
(parens && !semicolon ? ");" : ";") | ||
); | ||
}; | ||
const inspectNode = (node, context) => { | ||
const opts = context.options[0] || {}; | ||
if (isConstructor(node)) { | ||
return; | ||
} | ||
if ( | ||
!isClassMethod(node) && | ||
(containsThis(node.params) || containsThis(node.body)) | ||
) { | ||
return; | ||
} | ||
if (isGeneratorFunction(node)) { | ||
return; | ||
} | ||
if (isGetterOrSetter(node)) { | ||
return; | ||
} | ||
if (isClassMethod(node) && !opts.classPropertiesAllowed) { | ||
return; | ||
} | ||
if ( | ||
opts.allowStandaloneDeclarations && | ||
(isStandaloneDeclaration(node) || isModuleExport(node)) | ||
) { | ||
return; | ||
} | ||
if (opts.singleReturnOnly) { | ||
if ( | ||
functionOnlyContainsReturnStatement(node) && | ||
!isNamedDefaultExport(node) && | ||
(opts.classPropertiesAllowed || !isClassMethod(node)) | ||
) { | ||
return context.report({ | ||
node, | ||
message: | ||
"Prefer using arrow functions over plain functions which only return a value", | ||
fix: (fixer) => { | ||
const src = context.getSourceCode(); | ||
let newText = null; | ||
if (node.type === "FunctionDeclaration") { | ||
newText = fixFunctionDeclaration(src, node); | ||
} else if (node.type === "FunctionExpression") { | ||
newText = fixFunctionExpression(src, node); | ||
// In the case of an async method definition, we remove the "async" prefix | ||
if (node.async && node.parent.type === "MethodDefinition") { | ||
const parentTokens = src.getTokens(node.parent); | ||
const asyncToken = parentTokens.find( | ||
tokenMatcher("Identifier", "async"), | ||
); | ||
const nextToken = parentTokens.find((_, i, arr) => { | ||
return arr[i - 1] && arr[i - 1] === asyncToken; | ||
}); | ||
return [ | ||
fixer.replaceText(node, newText), | ||
fixer.replaceTextRange( | ||
[tokenStart(asyncToken), tokenStart(nextToken)], | ||
"", | ||
), | ||
]; | ||
} | ||
} | ||
if (newText !== null) { | ||
return fixer.replaceText(node, newText); | ||
} | ||
}, | ||
}); | ||
} | ||
} else if (opts.disallowPrototype || !isPrototypeAssignment(node)) { | ||
return context.report( | ||
node, | ||
isNamed(node) | ||
? "Use const or class constructors instead of named functions" | ||
: "Prefer using arrow functions over plain functions", | ||
); | ||
} | ||
}; | ||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: "prefer arrow functions", | ||
category: "emcascript6", | ||
recommended: false, | ||
}, | ||
fixable: "code", | ||
schema: [ | ||
{ | ||
type: "object", | ||
properties: { | ||
disallowPrototype: { | ||
type: "boolean", | ||
}, | ||
singleReturnOnly: { | ||
type: "boolean", | ||
}, | ||
classPropertiesAllowed: { | ||
type: "boolean", | ||
}, | ||
allowStandaloneDeclarations: { | ||
type: "boolean", | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
}, | ||
create: (context) => { | ||
return { | ||
"FunctionDeclaration:exit": (node) => { | ||
return inspectNode(node, context); | ||
}, | ||
"FunctionExpression:exit": (node) => { | ||
return inspectNode(node, context); | ||
}, | ||
}; | ||
}, | ||
}; |
@@ -1,2 +0,11 @@ | ||
const comparisonOperators = ["==", "===", "!=", "!==", ">", "<", ">=", "<="]; | ||
const comparisonOperators = new Set([ | ||
"==", | ||
"===", | ||
"!=", | ||
"!==", | ||
">", | ||
"<", | ||
">=", | ||
"<=", | ||
]); | ||
@@ -10,3 +19,3 @@ const enforceExplicitComparisonRecursively = (context, node) => { | ||
node.type === "BinaryExpression" && | ||
comparisonOperators.includes(node.operator) === true | ||
comparisonOperators.has(node.operator) === true | ||
) | ||
@@ -13,0 +22,0 @@ ) { |
@@ -12,3 +12,3 @@ module.exports = { | ||
message: "Use '++' instead of '+= 1'", | ||
fix: function (fixer) { | ||
fix: (fixer) => { | ||
return fixer.replaceText(node, `++${node.left.name}`); | ||
@@ -15,0 +15,0 @@ }, |
@@ -5,5 +5,5 @@ module.exports = { | ||
}, | ||
create: function (context) { | ||
create: (context) => { | ||
return { | ||
CallExpression(node) { | ||
CallExpression: (node) => { | ||
if ( | ||
@@ -16,6 +16,6 @@ node.callee.type === "MemberExpression" && | ||
const objectText = context | ||
.getSourceCode() | ||
.getText(node.callee.object), | ||
argument = node.arguments[0]; | ||
let value; | ||
.getSourceCode() | ||
.getText(node.callee.object); | ||
const [argument] = node.arguments; | ||
let val; | ||
@@ -26,6 +26,6 @@ if ( | ||
) { | ||
value = | ||
val = | ||
argument.operator === "-" | ||
? -argument.argument.value | ||
: +argument.argument.value; | ||
: Number(argument.argument.value); | ||
} else if ( | ||
@@ -35,18 +35,16 @@ argument.type === "Literal" && | ||
) { | ||
value = argument.value; | ||
val = argument.value; | ||
} | ||
let replacement; | ||
if (value === undefined) { | ||
if (val === undefined) { | ||
replacement = `${objectText}[${context | ||
.getSourceCode() | ||
.getText(argument)}]`; | ||
} else if (val >= 0) { | ||
replacement = `${objectText}[${val}]`; | ||
} else { | ||
if (value >= 0) { | ||
replacement = `${objectText}[${value}]`; | ||
} else { | ||
replacement = `${objectText}[${objectText}.length - ${Math.abs( | ||
value | ||
)}]`; | ||
} | ||
replacement = `${objectText}[${objectText}.length - ${Math.abs( | ||
val, | ||
)}]`; | ||
} | ||
@@ -57,3 +55,3 @@ | ||
message: "Use array indexing instead of .at()", | ||
fix(fixer) { | ||
fix: (fixer) => { | ||
return fixer.replaceText(node, replacement); | ||
@@ -60,0 +58,0 @@ }, |
725
26006
1