├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.js ├── lib │ ├── functional.js │ └── git.js └── lint-diff.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "add-module-exports", 5 | "ramda" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["pagarme-base"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | dist 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 9.11 5 | 6 | script: 7 | - npm run build 8 | - npm test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Guilherme Coelho 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lint-diff 2 | 3 | [![Build Status](https://travis-ci.org/grvcoelho/lint-diff.svg?branch=master)](https://travis-ci.org/grvcoelho/lint-diff) 4 | 5 | :nail_care: Run eslint only in the changed parts of the code 6 | 7 | ## Why 8 | 9 | [ESLint](https://github.com/eslint/eslint) is a great tool to enforce code 10 | style in your code, but it has some limitations: it can only lint entire files. 11 | When working with legacy code, we often have to make changes to very large 12 | files (which would be too troublesome to fix all lint errors)and thus it would 13 | be good to lint only the lines changed and not the entire file. 14 | 15 | [lint-diff](https://github.com/grvcoelho/lint-diff) receives a commit range and 16 | uses [ESLint](https://github.com/eslint/eslint) to lint the changed files and 17 | filter only the errors introduced in the commit range (and nothing more). 18 | 19 | ### State of the art 20 | 21 | * [lint-staged](https://github.com/okonet/lint-staged) is a similar tool that lints only the staged changes. It's very helpful for adding a precommit hook, but it cannot be used to enforce the styleguide on a Continuous Integration service like Travis, because the changes are already commited. 22 | 23 | ## Usage 24 | 25 | 1. Install it: 26 | 27 | ```sh 28 | $ npm install lint-diff 29 | ``` 30 | 31 | 2. Install `eslint` and add your eslint configuration file. 32 | 33 | 3. Use it: 34 | 35 | ```sh 36 | # This will lint the last commit 37 | $ lint-diff HEAD^..HEAD 38 | ``` 39 | 40 | ## Examples 41 | 42 | 1. Lint the last 3 commits: 43 | 44 | ```sh 45 | $ lint-diff HEAD~3..HEAD 46 | ``` 47 | 48 | 2. Lint local changes that are not yet commited (similar to what [lint-staged](https://github.com/okonet/lint-staged) do): 49 | 50 | ```sh 51 | $ lint-diff HEAD 52 | # or 53 | $ lint-diff 54 | ``` 55 | 56 | 3. Lint all commits from a build in [Travis](https://travis-ci.org): 57 | 58 | ```sh 59 | # This environment variable will be available in any Travis build 60 | $ lint-diff $TRAVIS_COMMIT_RANGE 61 | ``` 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lint-diff", 3 | "version": "1.2.0", 4 | "description": ":nail_care: Run eslint only in the changed parts of the code", 5 | "main": "./dist/lint-diff.js", 6 | "bin": "./dist/lint-diff.js", 7 | "scripts": { 8 | "build": "webpack", 9 | "lint": "eslint 'src/**/*.js'", 10 | "prepublish": "npm run build", 11 | "test": "npm run lint" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/grvcoelho/lint-diff.git" 16 | }, 17 | "keywords": [ 18 | "linter", 19 | "eslint", 20 | "diff", 21 | "git" 22 | ], 23 | "author": "Guilherme Coelho ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/grvcoelho/lint-diff/issues" 27 | }, 28 | "homepage": "https://github.com/grvcoelho/lint-diff#readme", 29 | "devDependencies": { 30 | "babel-loader": "^7.1.4", 31 | "babel-plugin-add-module-exports": "^0.2.1", 32 | "babel-plugin-ramda": "^1.6.1", 33 | "eslint": "^4.7.2", 34 | "eslint-config-pagarme-base": "^2.0.0", 35 | "eslint-plugin-import": "^2.7.0", 36 | "webpack": "^4.5.0", 37 | "webpack-cli": "^2.0.14", 38 | "webpack-node-externals": "^1.7.2" 39 | }, 40 | "dependencies": { 41 | "bluebird": "^3.5.1", 42 | "commander": "^2.15.1", 43 | "execa": "^0.10.0", 44 | "meow": "^5.0.0", 45 | "ramda": "^0.25.0" 46 | }, 47 | "peerDependencies": { 48 | "eslint": "4.x" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import meow from 'meow' 2 | import run from './lint-diff' 3 | 4 | const cli = meow(` 5 | Usage 6 | $ lint-diff [] 7 | 8 | Examples 9 | $ lint-diff 10 | $ lint-diff HEAD~1..HEAD 11 | $ lint-diff master..my-branch 12 | `) 13 | 14 | run(cli.input[0]) 15 | -------------------------------------------------------------------------------- /src/lib/functional.js: -------------------------------------------------------------------------------- 1 | import { 2 | addIndex, 3 | complement, 4 | curry, 5 | defaultTo, 6 | filter, 7 | insert, 8 | isEmpty, 9 | map, 10 | pipe, 11 | reduce, 12 | slice, 13 | startsWith, 14 | } from 'ramda' 15 | 16 | export const mapIndexed = addIndex(map) 17 | 18 | export const reduceIndexed = addIndex(reduce) 19 | 20 | export const firstItemStartsWith = curry((prefix, list) => 21 | startsWith(prefix, list[0])) 22 | 23 | export const doesNotStartWith = complement(startsWith) 24 | 25 | export const splitEveryTime = curry((predicate, list) => { 26 | const splitIndexes = pipe( 27 | reduceIndexed((acc, item, index) => { 28 | if (predicate(item)) { 29 | return [...acc, index] 30 | } 31 | 32 | return acc 33 | }, []), 34 | insert(list.length - 1, list.length) 35 | )(list) 36 | 37 | const split = mapIndexed((splitIndex, i, splitIndexList) => { 38 | const previousIndex = defaultTo(0, splitIndexList[i - 1]) 39 | const currentIndex = splitIndexList[i] 40 | 41 | return slice(previousIndex, currentIndex, list) 42 | }) 43 | 44 | return pipe( 45 | split, 46 | filter(complement(isEmpty)) 47 | )(splitIndexes) 48 | }) 49 | -------------------------------------------------------------------------------- /src/lib/git.js: -------------------------------------------------------------------------------- 1 | import { 2 | filter, 3 | flatten, 4 | map, 5 | pipe, 6 | split, 7 | startsWith, 8 | uniq, 9 | } from 'ramda' 10 | import { 11 | doesNotStartWith, 12 | firstItemStartsWith, 13 | splitEveryTime, 14 | } from './functional' 15 | 16 | export const getChangedLinesFromHunk = (hunk) => { 17 | let lineNumber = 0 18 | 19 | return hunk.reduce((changedLines, line) => { 20 | if (startsWith('@@', line)) { 21 | lineNumber = Number(line.match(/\+([0-9]+)/)[1]) - 1 22 | return changedLines 23 | } 24 | 25 | if (doesNotStartWith('-', line)) { 26 | lineNumber += 1 27 | 28 | if (startsWith('+', line)) { 29 | return [...changedLines, lineNumber] 30 | } 31 | } 32 | 33 | return changedLines 34 | }, []) 35 | } 36 | 37 | export const getHunksFromDiff = pipe( 38 | split('\n'), 39 | splitEveryTime(startsWith('@@')), 40 | filter(firstItemStartsWith('@@')) 41 | ) 42 | 43 | export const getChangedLinesFromDiff = pipe( 44 | getHunksFromDiff, 45 | map(getChangedLinesFromHunk), 46 | flatten, 47 | uniq 48 | ) 49 | -------------------------------------------------------------------------------- /src/lint-diff.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird' 2 | import exec from 'execa' 3 | import path from 'path' 4 | import { CLIEngine } from 'eslint' 5 | import { 6 | T, 7 | assoc, 8 | cond, 9 | curry, 10 | curryN, 11 | endsWith, 12 | evolve, 13 | equals, 14 | filter, 15 | find, 16 | length, 17 | map, 18 | merge, 19 | objOf, 20 | pipe, 21 | pipeP, 22 | pluck, 23 | prop, 24 | propEq, 25 | split, 26 | sum, 27 | tap, 28 | } from 'ramda' 29 | import { getChangedLinesFromDiff } from './lib/git' 30 | 31 | const linter = new CLIEngine() 32 | const formatter = linter.getFormatter() 33 | 34 | const getChangedFiles = pipeP( 35 | commitRange => exec('git', ['diff', commitRange, '--name-only', '--diff-filter=ACM']), 36 | prop('stdout'), 37 | split('\n'), 38 | filter(endsWith('.js')), 39 | map(path.resolve) 40 | ) 41 | 42 | const getDiff = curry((commitRange, filename) => 43 | exec('git', ['diff', commitRange, filename]) 44 | .then(prop('stdout'))) 45 | 46 | const getChangedFileLineMap = curry((commitRange, filePath) => pipeP( 47 | getDiff(commitRange), 48 | getChangedLinesFromDiff, 49 | objOf('changedLines'), 50 | assoc('filePath', filePath) 51 | )(filePath)) 52 | 53 | const lintChangedLines = pipe( 54 | map(prop('filePath')), 55 | linter.executeOnFiles.bind(linter) 56 | ) 57 | 58 | const filterLinterMessages = changedFileLineMap => (linterOutput) => { 59 | const filterMessagesByFile = (result) => { 60 | const fileLineMap = find(propEq('filePath', result.filePath), changedFileLineMap) 61 | const changedLines = prop('changedLines', fileLineMap) 62 | 63 | const filterMessages = evolve({ 64 | messages: filter(message => changedLines.includes(message.line)), 65 | }) 66 | 67 | return filterMessages(result) 68 | } 69 | 70 | const countBySeverity = severity => 71 | pipe( 72 | filter(propEq('severity', severity)), 73 | length 74 | ) 75 | 76 | const countWarningMessages = countBySeverity(1) 77 | const countErrorMessages = countBySeverity(2) 78 | 79 | const warningCount = (result) => { 80 | const transform = { 81 | warningCount: countWarningMessages(result.messages), 82 | } 83 | 84 | return merge(result, transform) 85 | } 86 | 87 | const errorCount = (result) => { 88 | const transform = { 89 | errorCount: countErrorMessages(result.messages), 90 | } 91 | 92 | return merge(result, transform) 93 | } 94 | 95 | return pipe( 96 | prop('results'), 97 | map(pipe( 98 | filterMessagesByFile, 99 | warningCount, 100 | errorCount 101 | )), 102 | objOf('results') 103 | )(linterOutput) 104 | } 105 | 106 | const applyLinter = changedFileLineMap => pipe( 107 | lintChangedLines, 108 | filterLinterMessages(changedFileLineMap) 109 | )(changedFileLineMap) 110 | 111 | const logResults = pipe( 112 | prop('results'), 113 | formatter, 114 | console.log 115 | ) 116 | 117 | const getErrorCountFromReport = pipe( 118 | prop('results'), 119 | pluck('errorCount'), 120 | sum 121 | ) 122 | 123 | const exitProcess = curryN(2, n => process.exit(n)) 124 | 125 | const reportResults = pipe( 126 | tap(logResults), 127 | getErrorCountFromReport, 128 | cond([ 129 | [equals(0), exitProcess(0)], 130 | [T, exitProcess(1)], 131 | ]) 132 | ) 133 | 134 | const run = (commitRange = 'HEAD') => Promise.resolve(commitRange) 135 | .then(getChangedFiles) 136 | .map(getChangedFileLineMap(commitRange)) 137 | .then(applyLinter) 138 | .then(reportResults) 139 | 140 | export default run 141 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { BannerPlugin } = require('webpack') 3 | const externals = require('webpack-node-externals') 4 | 5 | const config = { 6 | context: path.join(__dirname, './src'), 7 | entry: './index.js', 8 | output: { 9 | path: path.join(__dirname, './dist'), 10 | libraryTarget: 'commonjs2', 11 | filename: 'lint-diff.js', 12 | sourceMapFilename: 'lint-diff.js.map', 13 | }, 14 | devtool: 'source-map', 15 | target: 'node', 16 | externals: [externals()], 17 | mode: 'development', 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | use: 'babel-loader', 23 | exclude: /node_modules/, 24 | }, 25 | ], 26 | }, 27 | plugins: [ 28 | new BannerPlugin({ 29 | banner: '#!/usr/bin/env node', 30 | raw: true, 31 | }), 32 | ] 33 | } 34 | 35 | module.exports = config 36 | --------------------------------------------------------------------------------