Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/samples/unformatted.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"hello": "world"
}
58 changes: 56 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const AllowComments = 'allowComments';
const fileLintResults = {};
const fileComments = {};
const fileDocuments = {};
const fileLengths = {};

const getSignature = problem =>
`${problem.range.start.line} ${problem.range.start.character} ${problem.message}`;
Expand Down Expand Up @@ -104,6 +105,30 @@ const errorSignature = err =>

const getErrorCode = _.pipe(_.get('ruleId'), _.split('/'), _.last);

const preprocessorPlaceholder = '___';
const preprocessorTemplate = `JSON.stringify(${preprocessorPlaceholder})`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is JSON.stringify even necessary? Just wrapping the JSON is parentheses would make for valid JS: ({"foo":1})

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but as soon as additional eslint rules kick in (for example no-unused-expressions) or prettier tries to parse the file with babel, things start to fall apart. One could work around those issues by fine-tuning the eslintrc file, but just adding JSON.stringify simplifies things in practice a lot.


function mapFix(fix, fileLength, prefixLength, suffix) {
let text = fix.text;
// We have to map the fix in such a way, that we account for the removed prefix and suffix.
let range = fix.range.map(location => location - prefixLength);
// For the suffix we have three cases:
// 1) The fix ends before the suffix => nothing left to do
if (range[0] >= fileLength + suffix.length) {
// 2) The fix starts after the suffix (for example concerning the last line break)
range = range.map(location => location - suffix.length);
} else if (range[1] >= fileLength) {
// 3) The fix intersects the suffix
range[1] = Math.max(range[1] - suffix.length, fileLength);
// in that case we have to delete the suffix also from the fix text.
const suffixPosition = text.lastIndexOf(suffix);
if (suffixPosition >= 0) {
text = text.slice(0, suffixPosition) + text.slice(suffixPosition + suffix.length);
}
}
return {range, text};
}

const processors = {
'.json': {
preprocess: function(text, fileName) {
Expand All @@ -112,12 +137,27 @@ const processors = {
const parsed = jsonServiceHandle.parseJSONDocument(textDocument);
fileLintResults[fileName] = getDiagnostics(parsed);
fileComments[fileName] = parsed.comments;
return ['']; // sorry nothing ;)

const [, eol = ''] = text.match(/([\n\r]*)$/);
fileLengths[fileName] = text.length - eol.length;
return [
preprocessorTemplate.replace(
preprocessorPlaceholder,
text.slice(0, fileLengths[fileName])
) + eol
];
},
postprocess: function(messages, fileName) {
const textDocument = fileDocuments[fileName];
const fileLength = fileLengths[fileName];
delete fileLintResults[fileName];
delete fileComments[fileName];

const prefixLength = preprocessorTemplate.indexOf(preprocessorPlaceholder);
const suffix = preprocessorTemplate.slice(
prefixLength + preprocessorPlaceholder.length
);

return _.pipe(
_.first,
_.groupBy(errorSignature),
Expand All @@ -144,9 +184,23 @@ const processors = {
endColumn: error.endColumn + 1
});
}),
_.mapValues(error => {
if (_.startsWith('json/', error.ruleId)) return error;

const newError = _.assign(error, {
column: error.column - (error.line === 1 ? prefixLength : 0),
endColumn: error.endColumn - (error.endLine === 1 ? prefixLength : 0)
});
if (error.fix) {
newError.fix = mapFix(error.fix, fileLength, prefixLength, suffix);
}

return newError;
}),
_.values
)(messages);
}
},
supportsAutofix: true
}
};

Expand Down
12 changes: 12 additions & 0 deletions test/.eslintrc.with-prettier.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"plugins": ["self", "prettier"],
"rules": {
"prettier/prettier": ["error", {
"tabWidth": 4,
"useTabs": true,
"trailingComma": "all",
"singleQuote": true,
"endOfLine": "lf"
}]
}
}
36 changes: 30 additions & 6 deletions test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ const _ = require('lodash/fp');
const SCOPE = 'self'; // (for test purpose only, relying the the eslint-plugin-self for tests)
const scoped = rule => `${SCOPE}/${rule}`;

function getLintResults(filename, eslintConfig) {
function getLintResults(filename, eslintConfig, commands = []) {
try {
const results = execFileSync(
'eslint',
['--config', eslintConfig || 'custom.eslintrc.json', '--format', 'json', filename],
[
'--config',
eslintConfig || 'custom.eslintrc.json',
...commands,
'--format',
'json',
filename
],
{
encoding: 'utf8',
stdio: 'pipe',
Expand Down Expand Up @@ -45,10 +52,9 @@ function validateInfringementExpectation(expected, actualSituation) {
else expect(actualSituation).to.have.property(scoped(rule));
}
const allExpectedErrors = expected.map(_.pipe(_.split(':'), _.head, scoped));
expect(_.xor(_.keys(actualSituation), allExpectedErrors)).to.have.length(
0,
'Extra errors found'
);
// only check for errors generated by this plugin
const actualErrors = _.keys(actualSituation).filter(error => error.startsWith(scoped('')));
expect(_.xor(actualErrors, allExpectedErrors)).to.have.length(0, 'Extra errors found');
}

function validateFile(filename, config = {}) {
Expand All @@ -63,6 +69,15 @@ function validateFile(filename, config = {}) {
expect(results.warningCount).to.equal(config.warningCount, 'invalid counr of warnings');
}

function validateFixes(filename, config = {}) {
const result = getLintResults(`samples/${filename}.json`, config.eslintrc, ['--fix-dry-run']);

expect(result.output).not.to.be.undefined;
if (config.fixedOutput !== undefined) {
expect(result.output).to.equal(config.fixedOutput);
}
}

describe('Integrations tests', function() {
it('validate correct json', function() {
validateFile('good-json', {errorCount: 0, warningCount: 0});
Expand Down Expand Up @@ -119,3 +134,12 @@ describe('Integrations tests with config', function() {
});
});
});

describe('Integrations tests with Prettier', function() {
it('prettifies JSON file', function() {
validateFixes('unformatted', {
eslintrc: '.eslintrc.with-prettier.json',
fixedOutput: '{\n\t"hello": "world"\n}\n'
});
});
});
7 changes: 4 additions & 3 deletions test/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ describe('plugin', function() {
});
describe('preprocess', function() {
const preprocess = plugin.processors['.json'].preprocess;
it('should return the same text', function() {
it('should contain the text', function() {
const fileName = 'whatever-the-name.js';

const newText = preprocess('whatever', fileName);
const text = 'whatever';
const newText = preprocess(text, fileName);
assert.isArray(newText, 'preprocess should return array');
assert.strictEqual(newText[0], '');
assert.include(newText[0], text);
});
});
describe('postprocess', function() {
Expand Down