diff --git a/.gitignore b/.gitignore index 6704566..4de8844 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ dist # TernJS port file .tern-port + +# IntelliJ +.idea diff --git a/index.js b/index.js index 74c0996..c073c21 100755 --- a/index.js +++ b/index.js @@ -2,61 +2,54 @@ import eslint from './src/eslint.js'; import * as fs from 'fs'; +import {createFromEslintResult, getFilteredMessages} from './src/baseline.js'; const FILE_NAME = '.eslint-baseline.json'; -async function exec(){ - - if(!fs.existsSync(FILE_NAME)){ +async function exec() { + if (!fs.existsSync(FILE_NAME)) { console.log('baseline not found attempting to create...'); - let result = await eslint.execute(); - fs.appendFileSync(FILE_NAME, JSON.stringify(result, null, 4) ); + + const result = await eslint.execute(); + const baseline = createFromEslintResult(result); + + fs.appendFileSync(FILE_NAME, JSON.stringify(baseline, null, 4)); + console.log('baseline created successfully'); + process.exit(1); return; } - let baselineContent = fs.readFileSync(FILE_NAME); - let baseline = JSON.parse(baselineContent); - - // create the hash collection - let baselineHash = []; - baseline.forEach(x => baselineHash.push(x.hash)); + const baselineContent = fs.readFileSync(FILE_NAME); + const baseline = JSON.parse(baselineContent); - // run eslint - let result = await eslint.execute(); + // run eslint + const result = await eslint.execute(); - if(result === null) { + if (result === null) { console.error('eslint failed to run, baseline aborting...'); process.exit(1); } - + // compose eslint result with baseline - let fails = []; - result.forEach(x => { - - if(!baselineHash.includes(x.hash)){ - // new issue not in the baseline - fails.push(x); - } - }); + const fails = getFilteredMessages(result, baseline); console.info('eslint baseline compare results:') console.info(); - + // check results - if(fails.length > 0) { - for(let fail of fails){ - console.error(` - ${fail.path}, line ${fail.line}, rule ${fail.ruleId}, ${fail.message}`); + if (fails.length > 0) { + for (let fail of fails) { + console.error(` - ${fail.path}, line ${fail.line}:${fail.column}, rule ${fail.ruleId}, ${fail.message}`); } console.error(); console.error(` [fail] ${fails.length} issues found !!!`) process.exit(1); - }else{ + } else { console.info(' [OK] no issues found '); } - } -exec().catch(x => console.error(x)); \ No newline at end of file +exec().catch(x => console.error(x)); diff --git a/package-lock.json b/package-lock.json index 663dfee..31104d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@luminateone/eslint-baseline", - "version": "1.0.8", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@luminateone/eslint-baseline", - "version": "1.0.8", + "version": "2.0.0", "license": "MIT", "dependencies": { "execa": "^6.1.0", diff --git a/package.json b/package.json index 5c2198b..c69e07f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@luminateone/eslint-baseline", - "version": "1.0.9", + "version": "2.0.0", "description": "create a baseline for eslint pipelines", "keywords": [ "eslint", "baseline", "base-line", "ci", "code", "continuous integration", "linter", "quality"], "main": "index.js", @@ -33,6 +33,11 @@ "name": "LuminateOne", "email": "hello@luminate.one", "url": "https://www.luminateone.co.nz/" + }, + { + "name": "ChobotX", + "email": "ondrejchab@seznam.cz", + "url": "https://github.com/ChobotX" } ] } diff --git a/src/baseline.js b/src/baseline.js new file mode 100644 index 0000000..37b35ac --- /dev/null +++ b/src/baseline.js @@ -0,0 +1,162 @@ +import * as path from "path"; +import * as fs from 'fs'; +import * as objectHash from "object-hash"; + +/** + * Get trimmed lines from file, indexed by line number + * @param {string }filePath + * @returns {{[key: int]: string}} + */ +const getFileLinesTrimmed = (filePath) => { + const lines = {}; + + const file = fs.readFileSync(filePath, 'utf8'); + const fileLines = file.split("\n"); + for (const [index, line] of fileLines.entries()) { + lines[index + 1] = line.trimStart().trimEnd() + "\n"; + } + + return lines; +} + +/** + * Create baseline object from eslint result + * @param eslintResult + * @returns {{ + * [key: string]: { + * [key: string]: { + * path: string, + * context: string, + * ruleId: string, + * hash: string + * }[] + * } + * }} + */ +export const createFromEslintResult = (eslintResult) => { + const result = {}; + + const sortedFiles = Object.values(eslintResult).sort((a, b) => { + if (a.filePath < b.filePath) { + return -1; + } else if (a.filePath > b.filePath) { + return 1; + } else { + return 0; + } + }); + + for (const file of sortedFiles) { + if (file.errorCount === 0 && file.warningCount === 0) { + continue; + } + + const filePath = path.relative(process.cwd(), file.filePath); + const fileLines = getFileLinesTrimmed(file.filePath); + + if (!result[filePath]) { + result[filePath] = {}; + } + + const sortedMessages = file.messages.sort((a, b) => { + if (a.ruleId < b.ruleId) { + return -2; + } else if (a.ruleId > b.ruleId) { + return 2; + } else if (a.line < b.line) { + return -1; + } else if (a.line > b.line) { + return 1; + } else { + return 0; + } + }); + + for (const message of sortedMessages) { + if (!result[filePath][message.ruleId]) { + result[filePath][message.ruleId] = []; + } + + const context = ( + (fileLines[message.line - 1] ?? '') + + (fileLines[message.line] ?? '') + + (fileLines[message.line + 1] ?? '') + ); + + result[filePath][message.ruleId].push({ + path: filePath, + context, + ruleId: message.ruleId, + hash: objectHash.sha1({ + filePath, + ruleId: message.ruleId, + context, + }) + }); + } + } + + return result; +} + +/** + * Get eslint result messages that are not present in baseline + * @param eslintResult + * @param baseline + * @returns { + * { + * path: string, + * ruleId: string, + * line: int, + * column: int, + * message: string, + * }[] + * } + */ +export const getFilteredMessages = (eslintResult, baseline) => { + const fails = []; + + for (const file of Object.values(eslintResult)) { + const filePath = path.relative(process.cwd(), file.filePath); + + if (!baseline[filePath]) { + fails.push(...file.messages.map( + x => ({...x, path: filePath}) + )); + continue; + } + + const fileLines = getFileLinesTrimmed(file.filePath); + + for (const message of file.messages) { + if (!baseline[filePath][message.ruleId]) { + fails.push({ + ...message, + path: filePath, + }); + continue; + } + + const context = ( + (fileLines[message.line - 1] ?? '') + + (fileLines[message.line] ?? '') + + (fileLines[message.line + 1] ?? '') + ); + + const hash = objectHash.sha1({ + filePath, + ruleId: message.ruleId, + context, + }); + + if (!baseline[filePath][message.ruleId].some(x => x.hash === hash)) { + fails.push({ + ...message, + path: filePath, + }); + } + } + } + + return fails; +} diff --git a/src/eslint.js b/src/eslint.js index e871286..4e81150 100644 --- a/src/eslint.js +++ b/src/eslint.js @@ -1,6 +1,4 @@ -import { execa } from "execa"; -import * as path from 'path' -import * as objectHash from 'object-hash'; +import {execa} from "execa"; async function execute(args = []) { @@ -10,19 +8,16 @@ async function execute(args = []) { args.shift(); // remove command // default output to json, pass through all other args - args = ['eslint', '-f', 'json', ...args]; + args = ['eslint', '-f', 'json', ...args]; - - // run eslint let stdout = ''; try { - let result = await execa('npx', args, { env: { ...process.env } }); + let result = await execa('npx', args, {env: {...process.env}}); stdout = result.stdout; } catch (error) { + console.error(error.stderr); - // eslint throws an error on lint fail... - - if(error.exitCode !== 1) { + if (error.exitCode !== 1) { console.error(error.stderr); return null; } @@ -30,36 +25,9 @@ async function execute(args = []) { stdout = error.stdout; } - // extract rules - let json = JSON.parse(stdout); - let result = []; - json.forEach((file) => { - - file.messages.forEach((message) => { - if (message.severity >= 2) { - - let filePath = path.relative(process.cwd(), file.filePath); - - result.push({ - path: filePath, - line: message.line, - column: message.column, - ruleId: message.ruleId, - message: message.message, - hash: objectHash.sha1({ - filePath, - line: message.line, - column: message.column, // TODO: optional? - ruleId: message.ruleId - }) - }); - } - }); - }); - - return result; + return JSON.parse(stdout); } export default { execute -} \ No newline at end of file +}