Skip to content

Instantly share code, notes, and snippets.

@tw3
Last active April 12, 2019 04:29
Show Gist options
  • Select an option

  • Save tw3/7d710da95fb13ac1d4cfc41b965dd62e to your computer and use it in GitHub Desktop.

Select an option

Save tw3/7d710da95fb13ac1d4cfc41b965dd62e to your computer and use it in GitHub Desktop.
PR blocker script that returns outputs ESLint messages for lines that have been modified vs git master branch
const Git = require('nodegit');
const CLIEngine = require('eslint').CLIEngine;
/**
* Usage: node lint_modified_lines.js
*
* This script:
* 1. Determines the files and lines that have been added or changed compared to the master branch.
* 2. Runs eslint.
* 3. Filters the lint results to only the lines that have been added or changed.
*/
const DEBUG = true;
const REPO_PATH = './';
async function main() {
// Get mapping from the filePath -> modified lines for each file
const modifiedFileLines = await getModifiedFileLines(REPO_PATH);
try {
const report = await getEslintReport(modifiedFileLines);
console.log('SUCCESS');
console.log(report);
process.exit(0); // success
} catch (err) {
console.log('ERROR');
// The promise in getEslintReport() will reject() when linting fails
console.error(err);
process.exit(1); // failure
}
}
// --------------------------------
// getModifiedFileLines and helpers
// --------------------------------
async function getModifiedFileLines(repoPath) {
const modifiedFileLines = new Map();
const patches = await getDiffPatches(repoPath);
for (const patch of patches) {
const filePath = getFilePathFromPatch(patch);
if (!isLintableFileOrPatch(filePath, patch)) {
continue;
}
const modifiedLinesInFile = await getModifiedLinesFromPatch(patch);
if (modifiedLinesInFile.length > 0) {
modifiedFileLines.set(filePath, modifiedLinesInFile);
}
}
return modifiedFileLines;
}
async function getDiffPatches(repoPath) {
const repo = await Git.Repository.open(repoPath);
const head = await repo.getBranchCommit('master');
const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
const tree = await (head ? head.getTree() : Git.Tree.lookup(repo, emptyTree));
const diff = await Git.Diff.treeToWorkdir(repo, tree, {
flags: Git.Diff.OPTION.SHOW_UNTRACKED_CONTENT | Git.Diff.OPTION.RECURSE_UNTRACKED_DIRS,
contextLines: 0
});
const patches = await diff.patches();
return patches;
}
function getFilePathFromPatch(patch) {
let filePath;
const unresolvedFilePath = patch.newFile().path();
if (DEBUG) console.log('\nfilePath:', unresolvedFilePath);
try {
filePath = require.resolve(`../${unresolvedFilePath}`);
} catch (err) {
console.warn('WARNING: Could not resolve file (skipping)');
}
// if (DEBUG) console.log('resolved filePath:', filePath);
return filePath;
}
function isLintableFileOrPatch(filePath, patch) {
if (!filePath) {
return false;
}
const isLintableFile = (filePath.endsWith('.js') || filePath.endsWith('.ts') || filePath.endsWith('.json'));
if (!isLintableFile) {
if (DEBUG) console.log('File is not lintable based on the file path');
return false;
}
if (patch.isDeleted()) {
if (DEBUG) console.log('File is not lintable since it is deleted');
return false;
}
return true;
}
async function getModifiedLinesFromPatch(patch) {
const hunks = await patch.hunks();
const modifiedLines = [];
for (const hunk of hunks) {
const startLineNum = hunk.newStart();
const numNewLines = hunk.newLines();
if (numNewLines > 0) {
const endLineNum = startLineNum + numNewLines - 1;
const modifiedLineObj = { startLineNum, endLineNum };
if (DEBUG) console.log('modifiedLineObj', modifiedLineObj);
modifiedLines.push(modifiedLineObj);
}
}
return modifiedLines;
}
// ---------------------------
// getEslintReport and helpers
// ---------------------------
function getEslintReport(modifiedFileLines) {
const config = { configFile: require.resolve('../.eslintrc.js') };
const cli = new CLIEngine({
...config
});
const filePaths = Array.from(modifiedFileLines.keys());
return new Promise((resolve, reject) => {
const report = cli.executeOnFiles(filePaths);
const errorResults = CLIEngine.getErrorResults(report.results);
const modifiedErrorResults = [];
let hasError = false;
// Construct the array of modified error results
for (const errorResult of errorResults) {
const modifiedLinesInFile = modifiedFileLines.get(errorResult.filePath);
const modifiedErrorResult = getModifiedErrorResult(errorResult, modifiedLinesInFile);
if (modifiedErrorResult) {
hasError = (modifiedErrorResult.errorCount > 0);
modifiedErrorResults.push(modifiedErrorResult);
}
}
// Resolve or reject with formatted output
const formatter = cli.getFormatter('stylish'); // another format option: 'codeframe'
if (hasError) {
reject(formatter(modifiedErrorResults));
} else {
resolve(formatter(modifiedErrorResults));
}
});
}
function getModifiedErrorResult(errorResult, modifiedLines) {
if (!errorResult.messages) {
return undefined;
}
let counts = {
error: {
fixable: 0,
total: 0
},
warn: {
fixable: 0,
total: 0
}
};
// Filter down the messages to the lines that were modified (and tally counts)
const modifiedLineMessages = errorResult.messages.filter(message => {
const msgLineNum = message.line;
const isLineNumModified = modifiedLines.some(modifiedLineObj => {
return (
msgLineNum >= modifiedLineObj.startLineNum &&
msgLineNum <= modifiedLineObj.endLineNum
);
});
// Tally counts along the way
if (isLineNumModified) {
// console.log({message});
const obj = (message.severity === 1) ? counts.warn :
(message.severity === 2) ? counts.error :
{};
obj.total++;
if (message.hasOwnProperty('fix')) {
obj.fixable++;
}
}
return isLineNumModified;
});
if (modifiedLineMessages.length === 0) {
return undefined;
}
// console.log({errorResult});
return {
...errorResult,
errorCount: counts.error.total,
fixableErrorCount: counts.error.fixable,
warningCount: counts.warn.total,
fixableWarningCount: counts.warn.fixable,
messages: modifiedLineMessages
};
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment