├── src ├── index.js ├── rules │ ├── waterfall-imports.js │ ├── waterfall-requires.js │ └── waterfall-objects.js └── utils │ └── index.js ├── package.json ├── LICENSE └── README.md /src/index.js: -------------------------------------------------------------------------------- 1 | const WaterfallImports = require('./rules/waterfall-imports') 2 | const WaterfallObjects = require('./rules/waterfall-objects') 3 | const WaterfallRequires = require('./rules/waterfall-requires') 4 | 5 | module.exports = { 6 | rules: { 7 | 'waterfall-imports': WaterfallImports, 8 | 'waterfall-objects': WaterfallObjects, 9 | 'waterfall-requires': WaterfallRequires, 10 | } 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-waterfall", 3 | "version": "1.0.1", 4 | "description": "Write code like waterfall. more beautiful more readable", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "eslint", 11 | "eslint-plugin", 12 | "js" 13 | ], 14 | "author": "Ahmad Karimzade", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/ahmadkzx/eslint-waterfall.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/ahmadkzx/eslint-plugin-waterfall/issues" 22 | }, 23 | "homepage": "https://github.com/ahmadkzx/eslint-plugin-waterfall#readme" 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ahmad Karimzade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/rules/waterfall-imports.js: -------------------------------------------------------------------------------- 1 | const { sortByLength, getReplaceRange, getNodesTexts } = require('../utils') 2 | 3 | const WaterfallImports = { 4 | meta: { 5 | fixable: true, 6 | type: 'suggestion', 7 | docs: { 8 | recommended: true, 9 | category: 'Stylistic Issues', 10 | description: 'Sort all imports by line length', 11 | } 12 | }, 13 | 14 | create(context) { 15 | const src = context.getSourceCode() 16 | 17 | function isImportDeclaration(node) { 18 | return node.type === 'ImportDeclaration' 19 | } 20 | 21 | return { 22 | 'Program:exit': function(node) { 23 | const importDeclarations = node.body.filter(isImportDeclaration) 24 | if (importDeclarations.length === 0) return 25 | const sortedImportDeclarations = [...importDeclarations].sort((nodeA, nodeB) => sortByLength(nodeA, nodeB, src)) 26 | 27 | const importDeclarationsText = getNodesTexts(importDeclarations, src).join('') 28 | const sortedImportDeclarationsText = getNodesTexts(sortedImportDeclarations, src).join('') 29 | 30 | if (sortedImportDeclarationsText !== importDeclarationsText) { 31 | // Find imports that are out of order 32 | const outOfOrderImports = importDeclarations.filter((importNode, index) => { 33 | return importNode !== sortedImportDeclarations[index]; 34 | }); 35 | 36 | // Report on each out-of-order import 37 | if (outOfOrderImports.length > 0) { 38 | // We still need a single fix that replaces all imports 39 | const text = getNodesTexts(sortedImportDeclarations, src).join('\n') 40 | const range = getReplaceRange(importDeclarations) 41 | 42 | // Report on the first out-of-order import 43 | context.report({ 44 | node: outOfOrderImports[0], 45 | message: 'Imports should be sorted by line length', 46 | fix: function(fixer) { 47 | return fixer.replaceTextRange(range, text) 48 | } 49 | }) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | module.exports = WaterfallImports -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ESLint Plugin Waterfall 2 | Write code like waterfall. more beautiful more readable. 3 | Sort Object Properties, Imports and Requires by line length. 4 | 5 | ![ESLint Plugin Waterfall](https://user-images.githubusercontent.com/69081259/224570172-e0cd45ce-a55f-49d7-aaa5-9a466c3e6740.svg) 6 | 7 | ### Installation 8 | 9 | npm i eslint-plugin-waterfall --save-dev 10 | // or 11 | yan add eslint-plugin-watefall --dev 12 | 13 | ### Usage 14 | Add `waterfall` in plugins section of your eslint config: 15 | 16 | { 17 | "plugins": ["waterfall"] 18 | } 19 | 20 | Then configure the rules you want to use under the rules section: 21 | 22 | { 23 | "rules": { 24 | "waterfall/waterfall-objects": "error", 25 | "waterfall/waterfall-imports": "error", 26 | "waterfall/waterfall-requires": "error", 27 | } 28 | } 29 | 30 | ### Rules 31 | waterfall-objects: 32 | 33 | // ℹ️ Before: 34 | const person = { 35 | username: 'test', 36 | name: 'jason', 37 | email: 'json@gmail.com', 38 | country: 'usa', 39 | } 40 | 41 | // ✅ After: 42 | const peson = { 43 | name: 'jason', 44 | country: 'usa', 45 | username: 'test', 46 | email: 'jason@gmail.com', 47 | } 48 | 49 | waterfall-imports: 50 | 51 | // ℹ️ Before: 52 | import { useContext, useEffect, useState } from 'react' 53 | import Link from 'next/link' 54 | import { useRouter } from 'next/router' 55 | import axios from 'axios' 56 | import { getAccessToken, clearAuthCookies } from './auth' 57 | import Error from 'next/error' 58 | 59 | // ✅ After: 60 | import axios from 'axios' 61 | import Link from 'next/link' 62 | import Error from 'next/error' 63 | import { useRouter } from 'next/router' 64 | import { useContext, useEffect, useState } from 'react' 65 | import { getAccessToken, clearAuthCookies } from './auth' 66 | 67 | 68 | waterfall-requires: 69 | 70 | // ℹ️ Before: 71 | const path = require('path') 72 | const fs = require('fs') 73 | const express = require('express') 74 | const auth = require('./auth') 75 | 76 | // ✅ After: 77 | const fs = require('fs') 78 | const path = require('path') 79 | const auth = require('./auth') 80 | const express = reqiure('express') 81 | -------------------------------------------------------------------------------- /src/rules/waterfall-requires.js: -------------------------------------------------------------------------------- 1 | const { sortByLength, getReplaceRange, getNodesTexts } = require('../utils') 2 | 3 | const WaterfallRequires = { 4 | meta: { 5 | fixable: true, 6 | type: 'suggestion', 7 | docs: { 8 | recommended: true, 9 | category: 'Stylistic Issues', 10 | description: 'Sort all requires by line length', 11 | } 12 | }, 13 | 14 | create(context) { 15 | const src = context.getSourceCode() 16 | 17 | function isRequireDeclaration(node) { 18 | if (node.type === 'VariableDeclaration') { 19 | const text = src.getText(node) 20 | const textParts = text.split('=') // const test = require("test") => ['const test ', ' require("test")'] 21 | 22 | return textParts[1] && textParts[1].trim().startsWith('require(') 23 | 24 | } else if (node.type === 'ExpressionStatement') { 25 | // for requires expressions that not storing in a variable like: require('./index.css') 26 | return node.expression && node.expression.callee && node.expression.callee.name === 'require' 27 | } 28 | 29 | return false 30 | } 31 | 32 | return { 33 | 'Program:exit': function(node) { 34 | const requireDeclarations = node.body.filter(isRequireDeclaration) 35 | if (requireDeclarations.length === 0) return 36 | const sortedRequireDeclarations = [...requireDeclarations].sort((nodeA, nodeB) => sortByLength(nodeA, nodeB, src)) 37 | 38 | const requireDeclarationsText = getNodesTexts(requireDeclarations, src).join('') 39 | const sortedRequireDeclarationsText = getNodesTexts(sortedRequireDeclarations, src).join('') 40 | 41 | if (sortedRequireDeclarationsText !== requireDeclarationsText) { 42 | // Find requires that are out of order 43 | const outOfOrderRequires = requireDeclarations.filter((requireNode, index) => { 44 | return requireNode !== sortedRequireDeclarations[index]; 45 | }); 46 | 47 | // Report on the first out-of-order require 48 | if (outOfOrderRequires.length > 0) { 49 | // We still need a single fix that replaces all requires 50 | const text = getNodesTexts(sortedRequireDeclarations, src).join('\n') 51 | const range = getReplaceRange(requireDeclarations) 52 | 53 | context.report({ 54 | node: outOfOrderRequires[0], 55 | message: 'Requires should be sorted by line length', 56 | fix: function(fixer) { 57 | return fixer.replaceTextRange(range, text) 58 | } 59 | }) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | module.exports = WaterfallRequires -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param node - ESTree AST node 3 | * @param src - Source context 4 | * @returns {number} 5 | */ 6 | function getNodeLength(node, src) { 7 | const text = src.getText(node) 8 | return text.length 9 | } 10 | 11 | /** 12 | * @param nodeA - ESTree AST node 13 | * @param nodeB - ESTree AST node 14 | * @param src - Source context 15 | * @returns {number} 16 | */ 17 | function sortByLength(nodeA, nodeB, src) { 18 | const lengthA = getNodeLength(nodeA, src); 19 | const lengthB = getNodeLength(nodeB, src); 20 | return lengthA - lengthB; 21 | } 22 | 23 | /** 24 | * Get each node text and fix its first line indent 25 | * @param nodes - Array of ESTree AST nodes 26 | * @param src - Source context 27 | * @returns {string[]} 28 | */ 29 | function getNodesTexts(nodes, src) { 30 | return nodes.map(node => fixIndent(src.getText(node))) 31 | } 32 | 33 | /** 34 | * Get first node start to last node end range 35 | * After sorting nodes base on length we need to replace them with sorted ones 36 | * So first should calculate entire node text range (text start and end) 37 | * @param nodes - Array of ESTree AST nodes 38 | * @returns {number[]} 39 | */ 40 | function getReplaceRange(nodes) { 41 | return [nodes[0].range[0], nodes[nodes.length - 1].range[1]] 42 | } 43 | 44 | /** 45 | * Src.getText(node) returns node string but there is one problem 46 | * First line does not have correct indent 47 | * So we need to get last line indent and apply it to first line to solve problem 48 | * @param {string} str - The string to be fixed 49 | * @returns {string} 50 | */ 51 | function fixIndent(str) { 52 | const lines = str.split('\n') 53 | if (lines.length < 2) return str 54 | const lastLineIndent = lines[lines.length - 1].match(/^\s*/)[0] 55 | lines[0] = lastLineIndent + lines[0].trim() 56 | return lines.join('\n') 57 | } 58 | 59 | /** 60 | * Get object properties indent by catch first property indent 61 | * @param {string[]} properties 62 | * @param objectNode - ESTree AST nodes 63 | * @param src - Source context 64 | * @returns {string} 65 | */ 66 | function applyObjectPropertiesIndent(properties, objectNode, src) { 67 | const text = src.getText(objectNode) 68 | /* 69 | { 70 | test: 1, 71 | ... 72 | } 73 | */ 74 | const lines = text.split('\n') // => ['{', ' test: 1,', ...] 75 | const indentStr = lines[1].match(/^\s*/)[0] 76 | 77 | return properties.map((prop, i) => { 78 | if (i === 0 /*first prop always has indent*/) { 79 | return prop 80 | } else { 81 | return indentStr + prop 82 | } 83 | }) 84 | } 85 | 86 | 87 | 88 | module.exports = { 89 | fixIndent, 90 | sortByLength, 91 | getNodeLength, 92 | getNodesTexts, 93 | getReplaceRange, 94 | applyObjectPropertiesIndent 95 | } 96 | -------------------------------------------------------------------------------- /src/rules/waterfall-objects.js: -------------------------------------------------------------------------------- 1 | const { sortByLength, getReplaceRange, getNodesTexts, applyObjectPropertiesIndent } = require('../utils') 2 | 3 | const WaterfallObjects = { 4 | meta: { 5 | fixable: true, 6 | type: 'suggestion', 7 | docs: { 8 | recommended: true, 9 | category: 'Stylistic Issues', 10 | description: 'Sort all object properties by line length', 11 | } 12 | }, 13 | 14 | create(context) { 15 | const src = context.getSourceCode() 16 | 17 | function isObjectPattern(node) { 18 | return node.type === 'ObjectPattern' 19 | } 20 | 21 | function isExportDefaultDeclaration(node) { 22 | return node.type === 'ExportDefaultDeclaration' 23 | } 24 | 25 | function isNotSpreadElement(node) { 26 | return node.type !== 'SpreadElement' 27 | } 28 | 29 | function isMultiLine(node) { 30 | return src.getText(node).includes('\n') 31 | } 32 | 33 | function isSingleLine(node) { 34 | return !isMultiLine(node) 35 | } 36 | 37 | function WaterfallFunctionArgs(node) { 38 | if (!node.params) return 39 | 40 | const objectParams = node.params.filter(isObjectPattern).filter(isMultiLine) 41 | 42 | objectParams.forEach(objParam => { 43 | const properties = objParam.properties 44 | if (!properties || properties.length === 0) return 45 | const sortedProperties = [...properties].sort((nodeA, nodeB) => sortByLength(nodeA, nodeB, src)) 46 | 47 | const propertiesText = getNodesTexts(properties, src).join('') 48 | const sortedPropertiesText = getNodesTexts(sortedProperties, src).join('') 49 | 50 | if (sortedPropertiesText !== propertiesText) { 51 | // Find properties that are out of order 52 | const outOfOrderProperties = properties.filter((propNode, index) => { 53 | return propNode !== sortedProperties[index]; 54 | }); 55 | 56 | if (outOfOrderProperties.length > 0) { 57 | let text = getNodesTexts(sortedProperties, src) 58 | text = applyObjectPropertiesIndent(text, /*object node*/ node, src) 59 | text = text.join(',\n') 60 | const range = getReplaceRange(properties) 61 | 62 | context.report({ 63 | node: outOfOrderProperties[0], 64 | message: 'Object properties should be sorted by line length', 65 | fix: function(fixer) { 66 | return fixer.replaceTextRange(range, text) 67 | } 68 | }) 69 | } 70 | } 71 | }) 72 | } 73 | return { 74 | 'ObjectExpression:exit': function(node) { 75 | if (isSingleLine(node)) return 76 | if (isExportDefaultDeclaration(node.parent)) return // only for ignoring vue options api 77 | 78 | const properties = node.properties.filter(isNotSpreadElement) 79 | if (properties.length === 0) return 80 | const sortedProperties = [...properties].sort((nodeA, nodeB) => sortByLength(nodeA, nodeB, src)) 81 | 82 | const propertiesText = getNodesTexts(properties, src).join('') 83 | const sortedPropertiesText = getNodesTexts(sortedProperties, src).join('') 84 | 85 | if (sortedPropertiesText !== propertiesText) { 86 | // Find properties that are out of order 87 | const outOfOrderProperties = properties.filter((propNode, index) => { 88 | return propNode !== sortedProperties[index]; 89 | }); 90 | 91 | if (outOfOrderProperties.length > 0) { 92 | let text = getNodesTexts(sortedProperties, src) 93 | text = applyObjectPropertiesIndent(text, /*object node*/ node, src) 94 | text = text.join(',\n') 95 | const range = getReplaceRange(properties) 96 | 97 | context.report({ 98 | node: outOfOrderProperties[0], 99 | message: 'Object properties should be sorted by line length', 100 | fix: function(fixer) { 101 | return fixer.replaceTextRange(range, text) 102 | } 103 | }) 104 | } 105 | } 106 | }, 107 | 108 | 'MethodDefinition:exit': WaterfallFunctionArgs, 109 | 'FunctionDeclaration:exit': WaterfallFunctionArgs, 110 | 'ArrowFunctionExpression:exit': WaterfallFunctionArgs, 111 | } 112 | } 113 | } 114 | 115 | module.exports = WaterfallObjects --------------------------------------------------------------------------------