eslint-formatter-rdjson
Version:
ESLint Formatter for the Reviewdog Diagnostic Format (RDFormat)
181 lines (167 loc) • 5.66 kB
JavaScript
// ESLint Formatter to Output Reviewdog Diagnostic Format (RDFormat)
// https://github.com/reviewdog/reviewdog/blob/1d8f6d6897dcfa67c33a2ccdc2ea23a8cca96c8c/proto/rdf/reviewdog.proto
// https://github.com/eslint/eslint/blob/091e52ae1ca408f3e668f394c14d214c9ce806e6/lib/shared/types.js#L11
// https://github.com/eslint/eslint/blob/82669fa66670a00988db5b1d10fe8f3bf30be84e/lib/shared/config-validator.js#L40
function convertSeverity(s) {
if (s === 0) { // off
return 'INFO';
} else if (s === 1) {
return 'WARNING';
} else if (s === 2) {
return 'ERROR';
}
return 'UNKNOWN_SEVERITY';
}
function isHighSurrogate(ch) {
return 0xD800 <= ch && ch < 0xDC00;
}
function isLowSurrogate(ch) {
return 0xDC00 <= ch && ch < 0xE000;
}
function utf8length(str) {
let length = 0;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
if (isHighSurrogate(ch)) {
i++;
length += 4;
if (i >= str.length || !isLowSurrogate(str.charCodeAt(i))) {
throw new Error("invalid surrogate character");
}
} else if (isLowSurrogate(ch)) {
throw new Error("invalid surrogate character");
} else if (ch < 0x80) {
length++;
} else if (ch < 0x0800) {
length += 2;
} else {
length += 3;
}
}
return length;
}
function positionFromUTF16CodeUnitOffset(offset, text) {
const lines = text.split('\n');
let lnum = 1;
let column = 0;
let lengthSoFar = 0;
for (const line of lines) {
if (offset <= lengthSoFar + line.length) {
const lineText = line.slice(0, offset-lengthSoFar);
// +1 because eslint offset is a bit weird and will append text right
// after the offset.
column = utf8length(lineText) + 1;
break;
}
lengthSoFar += line.length + 1; // +1 for line-break.
lnum++;
}
return {line: lnum, column: column};
}
// How to get UTF-8 column from UTF-16 code unit column.
// 1. Extract the line text until the column (exclusive).
// This is important when the character at the column is surrogate pair.
// 2. Count length of the extracted line text in UTF-8.
// 3. +1 to the length to get the UTF-8 column.
//
// Example:
// - sourceLines: ["haya🐶🐱busa"]
// - line: 1
// - column: 7
// - Expected output: {line: 1, column: 9}
//
// Ref:
// - UTF-16 length("🐶"): 2
// - UTF-8 length("🐶"): 4
// v------- INPUT: {line: 1, column: 7}
// haya🐶🐱busa
// UTF-16 Column (input): 12345 7 9012
// UTF-8 Column (output): 12345 9 3456
// ~~~~~~ <= utf8length("haya🐶") = 8
//
// The given position points to "🐱" (line:1, column: 7)
// 1. Extract the line text until the column (exclusive): "haya🐶"
// 2. Count length of the extracted line text in UTF-8: utf8length("haya🐶") = 8
// 3. +1 to the length to get the UTF-8 column: 9
function positionFromLineAndUTF16CodeUnitOffsetColumn(line, column, sourceLines) {
let col = 0;
if (sourceLines.length >= line) {
// 1. Extract the line text until the column (exclusive)
const lineText = sourceLines[line-1].slice(0, column-1);
// 2&3. Count length of the extracted line text in UTF-8 and +1.
col = utf8length(lineText) + 1;
}
return {line: line, column: col};
}
function commonSuffixLength(str1, str2) {
let i = 0;
let seenSurrogate = false;
for (i = 0; i < str1.length && i < str2.length; ++i) {
const ch1 = str1.charCodeAt(str1.length-(i+1));
const ch2 = str2.charCodeAt(str2.length-(i+1));
if (ch1 !== ch2) {
if (seenSurrogate) {
if (!isHighSurrogate(ch1) || !isHighSurrogate(ch2)) {
throw new Error("invalid surrogate character");
}
// i is now between a low surrogate and a high surrogate.
// we need to remove the low surrogate from the common suffix
// to avoid breaking surrogate pairs.
i--;
}
break;
}
seenSurrogate = isLowSurrogate(ch1);
}
return i;
}
function buildMinimumSuggestion(fix, source) {
const l = commonSuffixLength(fix.text, source.slice(fix.range[0], fix.range[1]));
return {
range: {
start: positionFromUTF16CodeUnitOffset(fix.range[0], source),
end: positionFromUTF16CodeUnitOffset(fix.range[1] - l, source)
},
text: fix.text.slice(0, fix.text.length - l)
};
}
module.exports = function (results, data) {
const rdjson = {
source: {
name: 'eslint',
url: 'https://eslint.org/'
},
diagnostics: []
};
results.forEach(result => {
const filePath = result.filePath;
const source = result.source;
const sourceLines = source ? source.split('\n') : [];
result.messages.forEach(msg => {
const diagnostic = {
message: msg.message,
location: {
path: filePath,
range: {
start: positionFromLineAndUTF16CodeUnitOffsetColumn(msg.line, msg.column, sourceLines)
}
},
severity: convertSeverity(msg.severity),
code: {
value: msg.ruleId,
url: (data.rulesMeta[msg.ruleId] && data.rulesMeta[msg.ruleId].docs ? data.rulesMeta[msg.ruleId].docs.url : '')
},
original_output: JSON.stringify(msg)
};
// the end of the range is optional
if (msg.endLine && msg.endColumn) {
diagnostic.location.range.end = positionFromLineAndUTF16CodeUnitOffsetColumn(msg.endLine, msg.endColumn, sourceLines)
}
if (msg.fix) {
diagnostic.suggestions = [buildMinimumSuggestion(msg.fix, source)];
}
rdjson.diagnostics.push(diagnostic);
});
});
return JSON.stringify(rdjson);
};